@massimo-cassandro/minimo 1.33.0 → 1.33.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.
Files changed (84) hide show
  1. package/README.md +2 -1
  2. package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +1 -1
  3. package/dev-tools/starter-kit/source_files/app/assets/api_htaccess +20 -0
  4. package/dev-tools/starter-kit/source_files/{layout-critical.css → app/css/layout.critical.css} +1 -1
  5. package/dev-tools/starter-kit/source_files/app/css/layout.css +0 -0
  6. package/dev-tools/starter-kit/source_files/app/index.js +1 -0
  7. package/dev-tools/starter-kit/source_files/root/webpack-config-modules/inline-critical-css.mjs +41 -0
  8. package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-setup.mjs +1 -1
  9. package/dev-tools/starter-kit/{webpack.config.mjs → source_files/root/webpack.config.mjs} +50 -24
  10. package/dev-tools/starter-kit/starter-install.sh +141 -83
  11. package/dev-tools/stylelint-config/test/test.module.scss +9 -0
  12. package/package.json +1 -1
  13. package/snippets/php/iviewer.php +488 -0
  14. package/snippets/twig-symfony/ViewerController.php +549 -0
  15. package/src/custom-properties.css +29 -0
  16. package/src/minimo.css +3 -0
  17. package/src/utilities/dom-builder/dom-builder.js +9 -5
  18. package/src/web-components/json-table/json-table-component.js +387 -38
  19. package/src/web-components/json-table/json-table-component.module.css +190 -0
  20. package/src/web-components/json-table/json-table.minimo.tokens.mjs +195 -0
  21. package/src/web-components/json-table/readme.md +603 -82
  22. package/src/web-components/json-table/src/cell-content.js +136 -0
  23. package/src/web-components/json-table/src/content-utils.js +103 -0
  24. package/src/web-components/json-table/src/data-types.js +280 -0
  25. package/src/web-components/json-table/src/defaults.js +377 -58
  26. package/src/web-components/json-table/src/get-data.js +105 -10
  27. package/src/web-components/json-table/src/icons.js +80 -0
  28. package/src/web-components/json-table/src/info-section.js +85 -50
  29. package/src/web-components/json-table/src/main-builder.js +175 -52
  30. package/src/web-components/json-table/src/pagination.js +253 -0
  31. package/src/web-components/json-table/src/parse-cols.js +113 -0
  32. package/src/web-components/json-table/src/parse-rows.js +81 -0
  33. package/src/web-components/json-table/src/resolve-params.js +102 -32
  34. package/src/web-components/json-table/src/search.js +71 -0
  35. package/src/web-components/json-table/src/sorting.js +111 -0
  36. package/src/web-components/json-table/src/table-body.js +109 -0
  37. package/src/web-components/json-table/src/table-builder.js +152 -0
  38. package/src/web-components/json-table/src/table-tfoot.js +56 -0
  39. package/src/web-components/json-table/src/table-thead.js +183 -0
  40. package/src/web-components/json-table/src/update-info.js +43 -24
  41. package/src/web-components/s-datatable-component/{docs.md → readme.md} +5 -3
  42. package/src/web-components/s-datatable-component/s-datatable-component.js +1 -1
  43. package/types/global.d.ts +17 -3
  44. package/dev-tools/starter-kit/source_files/app_index.js +0 -1
  45. package/snippets/config-templates/_htaccess +0 -69
  46. package/snippets/config-templates/robots.txt +0 -5
  47. package/snippets/config-templates/rollup.config.mjs +0 -115
  48. /package/dev-tools/starter-kit/source_files/{_root_robots.txt → app/assets/robots.txt} +0 -0
  49. /package/dev-tools/starter-kit/source_files/{_root_htaccess → app/assets/root_htaccess} +0 -0
  50. /package/{src → dev-tools/starter-kit/source_files/app/css}/fonts.css +0 -0
  51. /package/dev-tools/starter-kit/source_files/{favicons-readme.md → app/favicons/favicons-readme.md} +0 -0
  52. /package/dev-tools/starter-kit/{webpack-template.ejs → source_files/app/tpl/index.ejs} +0 -0
  53. /package/dev-tools/starter-kit/source_files/{__project__.code-workspace → root/__project__.code-workspace} +0 -0
  54. /package/dev-tools/starter-kit/source_files/{_browserslistrc → root/_browserslistrc} +0 -0
  55. /package/dev-tools/starter-kit/source_files/{_editorconfig → root/_editorconfig} +0 -0
  56. /package/dev-tools/starter-kit/source_files/{_gitignore → root/_gitignore} +0 -0
  57. /package/dev-tools/starter-kit/source_files/{_prettierrc → root/_prettierrc} +0 -0
  58. /package/dev-tools/starter-kit/source_files/{eslint.config.mjs → root/eslint.config.mjs} +0 -0
  59. /package/dev-tools/starter-kit/source_files/{jsconfig.json → root/jsconfig.json} +0 -0
  60. /package/dev-tools/starter-kit/{package-tpl.json → source_files/root/package-tpl.json} +0 -0
  61. /package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +0 -0
  62. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/css-rules.mjs +0 -0
  63. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/get-jsConfig-aliases.mjs +0 -0
  64. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/mini-svg-data-uri-loader.cjs +0 -0
  65. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/postcss.config.mjs +0 -0
  66. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-variables-safelist.mjs +0 -0
  67. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svg-rules.mjs +0 -0
  68. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svgo.config.mjs +0 -0
  69. /package/snippets/{config-templates/_github → _github}/dependabot.yml +0 -0
  70. /package/snippets/{error-pages/error-pages.js → error-pages.js} +0 -0
  71. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.jsx +0 -0
  72. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.module.scss +0 -0
  73. /package/snippets/{config-templates → root}/_hintrc.cjs +0 -0
  74. /package/snippets/{config-templates → root}/_npmignore +0 -0
  75. /package/snippets/{config-templates → root}/_npmrc +0 -0
  76. /package/snippets/{config-templates → root}/_stylelintignore +0 -0
  77. /package/snippets/{config-templates → root}/babel.config--react.json +0 -0
  78. /package/snippets/{config-templates → root}/babel.config--solidJS.json +0 -0
  79. /package/snippets/{ImgViewerController.php → twig-symfony/ImgViewerController.php} +0 -0
  80. /package/snippets/{UnsplashController.php → twig-symfony/UnsplashController.php} +0 -0
  81. /package/snippets/{config-templates → twig-symfony}/_env.local +0 -0
  82. /package/snippets/{error-pages → twig-symfony}/bundles/TwigBundle/Exception/error.html.twig +0 -0
  83. /package/snippets/{flash-messages.html.twig → twig-symfony/flash-messages.html.twig} +0 -0
  84. /package/snippets/{minimo-form-layout.html.twig → twig-symfony/minimo-form-layout.html.twig} +0 -0
@@ -1,26 +1,42 @@
1
- # `<json-table>` – tabelle HTML da dati JSON
1
+ # `<json-table>` – datatable da JSON
2
2
 
3
- > **Work in progress.** In questa fase il componente genera solo la **struttura esterna**
4
- > (sezione info, input di ricerca, wrapper con `<table>` vuota e caption). Colonne (`cols`),
5
- > rendering delle righe, ordinamento, ricerca e paginazione **non sono ancora implementati**.
6
- > Il piano di sviluppo è in `src/web-components/TODO json-table/TODO.md`.
7
-
8
- Sostituisce `s-datatable-component`: nessuna dipendenza esterna, custom element
9
- light DOM (`<json-table>`).
3
+ **json-table** costruisce una tabella HTML da un array di dati JSON (inline o caricato via
4
+ fetch), a partire da una definizione delle colonne (`cols`) e da un insieme di tipi di dato
5
+ predefiniti ed estendibili, con ordinamento, ricerca e paginazione (lato client oppure, con
6
+ `serverSide: true`, delegati al server). È un web component light DOM senza dipendenze esterne:
7
+ può quindi essere stilizzato direttamente dal CSS del progetto in cui è utilizzato.
10
8
 
11
9
  ## Installazione
12
10
 
13
- Nessuna peer dependency. Basta importare il file del componente (registra il custom element):
11
+ È sufficiente importare il file del componente (registra il custom element):
14
12
 
15
13
  ```javascript
16
14
  import '@massimo-cassandro/minimo/src/web-components/json-table/json-table-component.js';
17
- // oppure, se serve la classe (es. per setDefaults):
15
+
16
+ // oppure, se necessario eseguire metodi della classe (es. `setDefaults`):
18
17
  import { JsonTable } from '@massimo-cassandro/minimo/src/web-components/json-table/json-table-component.js';
19
18
  ```
20
19
 
21
20
  Il CSS (`json-table-component.module.css`) è importato dal componente stesso; le custom
22
21
  properties `--jt-*` sono definite in `json-table.minimo.tokens.mjs` e compilate in
23
- `custom-properties.css` da `build-tokens`.
22
+ `custom-properties.css` da `build-tokens`. Le icone di default (booleani, ordinamento e
23
+ paginazione) sono SVG di minimo importati con il suffisso `?inline` (gestito dalla configurazione
24
+ webpack dello starter-kit).
25
+
26
+ ### Tipi per gli script consumer
27
+
28
+ Il file `types/global.d.ts` di minimo dichiara il tipo globale `JsonTable` e registra il tag
29
+ `json-table` in `HTMLElementTagNameMap`: aggiungendolo all'`include` del proprio
30
+ `jsconfig.json`/`tsconfig.json` (`"node_modules/@massimo-cassandro/minimo/types/global.d.ts"`)
31
+ si ottiene il completamento dei metodi senza cast né import di tipo:
32
+
33
+ ```javascript
34
+ const el = document.querySelector('json-table'); // → JsonTable
35
+ el.init({ ... });
36
+
37
+ /** @type {JsonTable} */
38
+ const other = document.createElement('json-table');
39
+ ```
24
40
 
25
41
  ## Utilizzo da markup
26
42
 
@@ -28,21 +44,35 @@ properties `--jt-*` sono definite in `json-table.minimo.tokens.mjs` e compilate
28
44
  <json-table
29
45
  jsonurl="/api/rows.json"
30
46
  caption="Utenti"
47
+ perpage="10"
48
+ cols='[
49
+ { "key": "id", "dataType": "id" },
50
+ { "key": "name", "title": "Nome", "render": "<a href=\"/users/[[id]]\">[[name]]</a>" },
51
+ { "key": "amount", "title": "Importo", "dataType": "euro", "tfootRender": "@sum" },
52
+ { "key": "active", "title": "Attivo", "dataType": "bool" }
53
+ ]'
54
+ tfoot="true"
31
55
  ></json-table>
32
56
 
33
- <!-- dati inline (JSON serializzato, utile in contesti SSR: Twig, Blade, ...) -->
57
+ <!-- dati inline (JSON serializzato) al posto di jsonurl -->
34
58
  <json-table
35
59
  data='[{"name":"Mario","city":"Roma"}, ...]'
60
+ cols='[{ "key": "name", "title": "Nome" }, { "key": "city", "title": "Città" }]'
36
61
  search="false"
37
62
  ></json-table>
38
63
  ```
39
64
 
40
65
  I nomi degli attributi sono case-insensitive (`jsonurl` e `jsonUrl` sono equivalenti).
41
66
  Gli attributi booleani accettano i formati HTML idiomatici: `search` (presente senza valore),
42
- `search="true"`, `search="1"` → true; `search="false"`, `search="0"` → false.
67
+ `search="true"`, `search="1"` → true; `search="false"`, `search="0"` → false. Gli attributi
68
+ numerici (`perpage`, `paginationdelta`, `searchdebounce`) vengono convertiti in numero.
43
69
  I valori che iniziano con `[` o `{` vengono interpretati come JSON (un JSON malformato viene
44
70
  segnalato in console e ignorato).
45
71
 
72
+ Da attributo non è possibile passare funzioni: per il rendering delle celle è disponibile la
73
+ sintassi mustache-like `[[key]]` (vedi [`render`](#render)), per il footer gli aggregati
74
+ predefiniti (`"@sum"`, ...).
75
+
46
76
  ## Utilizzo da script
47
77
 
48
78
  ```javascript
@@ -51,12 +81,20 @@ const el = document.querySelector('json-table');
51
81
 
52
82
  el.init({
53
83
  jsonUrl: '/api/rows.json',
54
- caption: 'Utenti'
84
+ caption: 'Utenti',
85
+ cols: [
86
+ { key: 'id', dataType: 'id' },
87
+ { key: 'name', title: 'Nome', render: row => `<a href="/users/${row.id}">${row.name}</a>` },
88
+ { key: 'amount', title: 'Importo', dataType: 'euro', tfootRender: '@sum' },
89
+ { key: 'active', title: 'Attivo', dataType: 'bool' }
90
+ ],
91
+ tfoot: true
55
92
  });
56
93
 
57
94
  // dati inline al posto di jsonUrl (se presenti entrambi, `data` prevale)
58
95
  el.init({
59
- data: [{ name: 'Mario', city: 'Roma' }, ...]
96
+ data: [{ name: 'Mario', city: 'Roma' }, ...],
97
+ cols: [...]
60
98
  });
61
99
 
62
100
  // se appena creato, appendere dopo init():
@@ -70,7 +108,7 @@ domBuilder([
70
108
  {
71
109
  tag: 'json-table',
72
110
  attrs: { caption: 'Utenti' },
73
- callback: el => el.init({ jsonUrl: '/api/rows.json' })
111
+ callback: el => el.init({ jsonUrl: '/api/rows.json', cols: [...] })
74
112
  }
75
113
  ], container);
76
114
  ```
@@ -80,19 +118,26 @@ domBuilder([
80
118
  I parametri non inclusi in `init()` vengono letti dall'attributo HTML corrispondente:
81
119
 
82
120
  ```html
83
- <json-table caption="Utenti" search="false"></json-table>
121
+ <json-table caption="Utenti" search="false" cols='[...]'></json-table>
84
122
  ```
85
123
 
86
124
  ```javascript
87
- el.init({ jsonUrl: '/api/rows.json' }); // jsonUrl da script, caption e search da attributo
125
+ el.init({ jsonUrl: '/api/rows.json' }); // jsonUrl da script, caption, search e cols da attributo
88
126
  ```
89
127
 
90
128
  **Ordine di precedenza:** `init()` > attributo HTML > `JsonTable.setDefaults()` > default interno.
91
129
 
92
- Per ignorare esplicitamente un attributo e ricadere sui default, passare `null` nel config:
130
+ Per ignorare esplicitamente un attributo e utilizzare il valore di default, utilizzare `null` nel config:
131
+
132
+ ```javascript
133
+ el.init({ jsonUrl: '/api/rows.json', search: null }); // search == default (true)
134
+ ```
135
+
136
+ I parametri oggetto `classes`, `labels`, `dataTypes` e `serverParams` vengono **uniti** ai default
137
+ (merge a un livello), quindi ogni sorgente deve indicare solo le chiavi da sovrascrivere:
93
138
 
94
139
  ```javascript
95
- el.init({ jsonUrl: '/api/rows.json', search: null }); // search → default (true)
140
+ el.init({ classes: { table: 'table' } }); // le altre classi restano quelle di default
96
141
  ```
97
142
 
98
143
  ## Default di progetto – `JsonTable.setDefaults()`
@@ -105,9 +150,10 @@ I valori vengono uniti a quelli impostati in chiamate precedenti.
105
150
  import { JsonTable } from '.../json-table-component.js';
106
151
 
107
152
  JsonTable.setDefaults({
108
- searchInputClass: 'form-control form-control-sm',
109
- tableClass: 'table',
110
- infoText: (shown, total) => `${shown} di ${total} record`
153
+ perPage: 50,
154
+ classes: { searchInput: 'form-control', table: 'table' },
155
+ labels: { searchPlaceholder: 'Cerca…', noRows: 'Nessun elemento' },
156
+ infoText: (start, end, totRec, filteredRec) => `${filteredRec} di ${totRec} record`
111
157
  });
112
158
 
113
159
  JsonTable.getDefaults(); // default interni + default di progetto
@@ -119,115 +165,561 @@ JsonTable.resetDefaults(); // azzera i default di progetto
119
165
  | Metodo | Descrizione |
120
166
  |---|---|
121
167
  | `init(config)` | Avvia il componente (prima o dopo l'inserimento nel DOM) |
122
- | `reload(overrides)` | Ricarica i dati e ricostruisce la struttura, con eventuali parametri sovrascritti (`Promise`) |
168
+ | `reload(overrides)` | Ricarica i dati e ricostruisce la struttura, con eventuali parametri sovrascritti (`Promise`); ricerca, ordinamento e pagina vengono azzerati |
123
169
  | `destroy()` | Svuota il componente; `init()` può essere richiamato in seguito |
170
+ | `goToPage(page)` | Mostra la pagina indicata (limitata all'intervallo disponibile) |
171
+ | `setSort(key, dir)` | Ordina per la colonna `key` (`'asc'`, `'desc'`, oppure `null` per rimuovere l'ordinamento) e torna alla prima pagina |
172
+ | `setSearch(term)` | Filtra le righe con il termine indicato (stringa vuota = nessun filtro) e torna alla prima pagina; l'input di ricerca viene aggiornato |
173
+
174
+ In modalità server-side `goToPage`, `setSort` e `setSearch` generano una nuova richiesta.
124
175
 
125
- Proprietà disponibili dopo il caricamento: `params` (parametri risolti), `data` (righe grezze),
126
- `elements` (elementi generati: `wrapper`, `infoOuter`, `info`, `resultInfo`, `searchInput`,
127
- `tableWrapper`, `table`).
176
+ Proprietà disponibili dopo il caricamento:
177
+
178
+ | Proprietà | Descrizione |
179
+ |---|---|
180
+ | `params` | parametri risolti |
181
+ | `data` | righe grezze (array di oggetti; in modalità server-side solo quelle della pagina corrente) |
182
+ | `cols` | colonne dopo il parsing (solo quelle visibili, con classi e tipo di dato risolti) |
183
+ | `dataTypes` | mappa dei tipi di dato (predefiniti + personalizzati) |
184
+ | `state` | stato del rendering: `totRec`, `filteredRec`, `rows`, `filtered`, `pageRows`, `searchTerm`, `sort` (`{ key, dir }` o `null`), `page`, `totPages` |
185
+ | `elements` | elementi generati: `wrapper`, `infoOuter`, `info`, `resultInfo`, `searchInput`, `tableWrapper`, `table`, `thead`, `tbody`, `tfoot`, `tableFooter`, `caption`, `pagination` |
128
186
 
129
187
  ```javascript
130
188
  await el.reload({ jsonUrl: '/api/rows.json?anno=2025' });
131
189
  await el.reload({ data: [...] });
190
+ el.setSort('name', 'desc');
191
+ el.goToPage(3);
132
192
  ```
133
193
 
134
- ## Evento `jt:ready`
194
+ ## Eventi
135
195
 
136
- Emesso sull'elemento (con `bubbles`) al termine della costruzione; `event.detail.jsonTable`
137
- è l'istanza. Viene emesso nel microtask successivo, quindi un listener registrato subito dopo
138
- `init()` lo intercetta comunque.
196
+ Entrambi vengono emessi sull'elemento con `bubbles: true`; `event.detail.jsonTable` è l'istanza.
197
+
198
+ | Evento | Quando | `event.detail` |
199
+ |---|---|---|
200
+ | `jt:ready` | al termine della costruzione e del primo rendering (nel microtask successivo, quindi un listener registrato subito dopo `init()` lo intercetta comunque) | `{ jsonTable }` |
201
+ | `jt:update` | dopo ogni cambio pagina, ordinamento o ricerca (anche via API) | `{ jsonTable, reason }` con `reason` = `'page'`, `'sort'` o `'search'` |
139
202
 
140
203
  ```javascript
141
- el.addEventListener('jt:ready', e => {
142
- console.log(e.detail.jsonTable.data);
143
- });
144
- el.init({ jsonUrl: '/api/rows.json' });
204
+ el.addEventListener('jt:ready', e => console.log(e.detail.jsonTable.data));
205
+ el.addEventListener('jt:update', e => console.log(e.detail.reason, e.detail.jsonTable.state.page));
206
+ el.init({ jsonUrl: '/api/rows.json', cols: [...] });
145
207
  ```
146
208
 
147
209
  ## Parametri
148
210
 
149
211
  Tutti i parametri sono impostabili sia da attributo HTML che da `init()`. I valori di tipo
150
- funzione (`caption`, `infoText`) solo da `init()`.
212
+ funzione solo da `init()`.
151
213
 
152
214
  ```javascript
153
215
  {
154
- debug, // boolean – log in console di parametri, dati ed elementi generati.
155
- // Default: false
216
+ debug, // boolean – log in console di parametri, colonne, dati, stato ed
217
+ // elementi generati. Default: false
156
218
 
157
219
  jsonUrl, // string – URL del JSON da caricare. Ignorato se `data` è presente.
158
220
  // Default: null
159
-
160
221
  jsonDataField, // string|null – chiave del JSON che contiene l'array delle righe
161
222
  // (es. 'data' per { data: [...] }); null o '' = la root del JSON è
162
223
  // l'array stesso. Default: 'data'
163
-
224
+ totRecField, // string – chiave del JSON con il totale dei record (numerico); se
225
+ // assente o non numerico il totale è la lunghezza dell'array.
226
+ // Default: 'totRec'
227
+ filteredRecField, // string – solo server-side: chiave del JSON con il numero di record
228
+ // che soddisfano la ricerca corrente; se assente vale `totRecField`.
229
+ // Default: 'filteredRec'
164
230
  data, // Array<Object> – righe inline; prevale su `jsonUrl`. Da attributo va
165
231
  // passato come stringa JSON ed è sempre inteso come array delle righe
166
232
  // (`jsonDataField` ignorato). Default: null
167
233
 
168
- caption, // string|Function – caption della tabella (testo o HTML), oppure
169
- // funzione che restituisce stringa o Node. Default: null
234
+ cols, // ColDefinition[] – OBBLIGATORIO: definizione delle colonne (vedi
235
+ // sotto). Se assente o vuoto viene segnalato un errore in console e
236
+ // la tabella non viene generata
237
+ dataTypes, // Object – tipi di dato personalizzati, uniti a quelli predefiniti
238
+ // (vedi sotto). Default: {}
170
239
 
240
+ caption, // string|Function – caption della tabella (testo o HTML), oppure
241
+ // funzione che restituisce stringa o Node. Mostrata sotto la tabella
242
+ // (vedi "Caption e paginazione"). Default: null
171
243
  search, // boolean – genera l'input di ricerca. Default: true
172
- searchInputClass, // string – classi dell'input di ricerca (sostituisce il default).
173
- // Default: 'form-control'
174
- searchInputTitle, // string – attributo title dell'input.
175
- // Default: 'Filtra record: inserisci un termine per eseguire la ricerca'
176
- searchInputPlaceholder, // string – placeholder dell'input.
177
- // Default: 'Inserisci il termine da cercare'
178
- searchInputAriaLabel, // string – aria-label dell'input. Default: 'Filtra risultati'
179
-
244
+ searchDebounce, // number – ms di attesa dopo l'ultimo tasto prima di eseguire la
245
+ // ricerca. Default: 300
246
+
247
+ perPage, // number – righe per pagina; 0 = nessuna paginazione (tutte le righe,
248
+ // nessuna navigazione). Default: 25
249
+ paginationDelta, // number – pulsanti pagina mostrati a ciascun lato della pagina
250
+ // corrente. Default: 2
251
+ serverSide, // boolean – paginazione, ordinamento e ricerca delegati al server
252
+ // (vedi "Modalità server-side"). Richiede `jsonUrl`. Default: false
253
+ serverParams, // Object – nomi dei parametri di query string in modalità server-side,
254
+ // uniti ai default (vedi sotto)
255
+ initialSort, // { key, dir }|null – ordinamento applicato al caricamento, es.
256
+ // { key: 'name', dir: 'asc' }. Default: null
257
+
258
+ tfoot, // boolean – genera il tfoot (vedi `tfootRender` delle colonne).
259
+ // Non disponibile in modalità server-side. Default: false
260
+ updateFooterOnPageChange, // boolean – se true `tfootRender` riceve solo i record della pagina
261
+ // corrente (subtotali di pagina) e il footer si aggiorna a ogni cambio
262
+ // pagina; se false riceve l'intero set filtrato e si aggiorna solo al
263
+ // cambio filtro. Default: false
264
+ infoText, // string|Function|null – contenuto dell'area info: stringa mustache-like
265
+ // con i segnaposto {start}, {end}, {totRec}, {filteredRec}, {page},
266
+ // {totPages}, oppure funzione
267
+ // (start, end, totRec, filteredRec, page, totPages) => string|Node.
268
+ // null = viene usato `labels.info`. Default: null
269
+ template, // Array – layout del contenitore principale (vedi "Template").
270
+ // Default: [{ slot: 'infoSection' }, { slot: 'table' }]
271
+
272
+ locale, // string – locale per numeri, date e confronto stringhe
273
+ // nell'ordinamento. Default: 'it-IT'
274
+ currency, // string – codice ISO 4217 usato dal tipo `currency`. Default: 'EUR'
275
+ datesLocaleOpts, // Intl.DateTimeFormatOptions – parte data dei tipi date/datetime.
276
+ // Default: { year: 'numeric', month: 'short', day: 'numeric' }
277
+ timesLocaleOpts, // Intl.DateTimeFormatOptions – parte ora del tipo datetime.
278
+ // Default: { hour12: false, hour: '2-digit', minute: '2-digit' }
279
+ numbersLocaleOpts, // Intl.NumberFormatOptions – tipo num.
280
+ // Default: { maximumFractionDigits: 2 }
281
+ currPercLocaleOpts, // Intl.NumberFormatOptions – tipi currency/euro/perc/percDecimal.
282
+ // Default: { minimumFractionDigits: 2, maximumFractionDigits: 2 }
283
+
284
+ renderNullAs, // string|null – contenuto per i valori null/undefined. Default: '—'
285
+ renderZeroAs, // string|null – se non null, contenuto per i valori numerici pari a
286
+ // zero. Default: null
287
+ renderNaNAs, // string|null – contenuto per i valori non numerici nei tipi numerici.
288
+ // Default: '—'
289
+
290
+ boolTrueIcon, // icona per i valori true (tipo bool).
291
+ // Default: icona `check-bold` di minimo (Phosphor)
292
+ boolFalseIcon, // icona per i valori false. Default: icona `x-bold`
293
+ sortAscArrowIcon, // icona del pulsante di ordinamento, ordinamento asc attivo.
294
+ // Default: icona `arrow-up`
295
+ sortDescArrowIcon, // idem, ordinamento desc attivo. Default: icona `arrow-down`
296
+ sortNoneArrowIcon, // idem, nessun ordinamento. Default: icona `arrows-down-up`
297
+ paginationPrevIcon, // icona del pulsante "pagina precedente". Default: icona `caret-left`
298
+ paginationNextIcon, // icona del pulsante "pagina successiva". Default: icona `caret-right`
299
+ // Ogni icona può essere una stringa SVG/HTML (es. import `?inline`),
300
+ // un Node, un oggetto domBuilder o una funzione che restituisce uno
301
+ // di questi valori
302
+
303
+ trCallback, // (tr, row, params) => void – callback invocata dopo il rendering di
304
+ // ogni riga del body. Default: null
180
305
  tableId, // string – id del tag <table>. Default: null
181
- tableWrapperClass, // string – classi del div che racchiude la tabella (sostituisce il
182
- // default). Default: 'table-responsive'
183
- tableClass, // string – classi del tag <table> (sostituisce il default).
184
- // Default: 'table table-bordered'
185
-
186
- mainWrapperExtraClass, // string – classi aggiuntive per il contenitore principale
187
- // (info + tabella). Default: null
188
- outerInfoExtraClass, // string – classi aggiuntive per il contenitore esterno della
189
- // sezione info. Default: null
190
- infoExtraClass, // string – classi aggiuntive per la sezione info. Default: null
191
-
192
- infoText, // (shown, total) => string|Node – testo dell'area info (testo,
193
- // stringa HTML o Node).
194
- // Default: "Visualizzate N righe su M" / "Nessun record"
306
+
307
+ classes, // Object – classi consumer (vedi sotto), unite ai default
308
+ labels // Object – testi (vedi sotto), uniti ai default
195
309
  }
196
310
  ```
197
311
 
198
- I parametri `*ExtraClass` si **aggiungono** alle classi interne di layout; i parametri
199
- `*Class` (`searchInputClass`, `tableWrapperClass`, `tableClass`) **sostituiscono** interamente
200
- il valore di default, ferme restando le eventuali classi interne di layout del componente.
312
+ ### `classes`
313
+
314
+ Nomi delle classi assegnate agli elementi generati. Le classi interne di layout (CSS module)
315
+ vengono **sempre** applicate in aggiunta a queste; ogni valore sostituisce interamente il
316
+ default della stessa chiave.
317
+
318
+ ```javascript
319
+ classes: {
320
+ wrapper: null, // contenitore principale (<section>: info + tabella)
321
+ infoOuter: null, // contenitore esterno della sezione info
322
+ info: null, // sezione info (testo + ricerca)
323
+ resultInfo: null, // contenitore del testo info
324
+ search: null, // wrapper dell'input di ricerca
325
+ searchInput: 'form-control form-control-sm', // input di ricerca
326
+ tableWrapper: 'table-responsive', // div che racchiude la tabella
327
+ table: 'table table-bordered', // tag <table>
328
+ tableFooter: null, // barra sotto la tabella (caption + paginazione)
329
+ caption: null, // contenitore della caption
330
+ pagination: null, // <nav> della paginazione
331
+ paginationBtn: 'btn-reset', // pulsanti della paginazione
332
+ sortBtn: 'btn-reset', // pulsanti di ordinamento nei <th>
333
+ empty: null, // cella unica mostrata in assenza di righe
334
+ textStart: null, // allineamento inline-start (le celle minimo lo sono già)
335
+ textCenter: 'text-center', // allineamento al centro
336
+ textEnd: 'text-end', // allineamento inline-end
337
+ nowrap: 'text-nowrap', // no-wrap
338
+ numeric: 'text-numeric', // cifre tabulari (tipi numerici)
339
+ boolCell: null, // classe aggiuntiva per tutte le celle di tipo bool
340
+ boolTrue: null, // classe aggiuntiva per le celle bool con valore true
341
+ boolFalse: null // classe aggiuntiva per le celle bool con valore false
342
+ }
343
+ ```
344
+
345
+ Le classi di allineamento (`textEnd`, `textCenter`, `nowrap`, `numeric`) sono quelle usate dai
346
+ tipi di dato predefiniti: modificandole si cambiano le classi di tutte le colonne di quel tipo.
347
+
348
+ ### `labels`
349
+
350
+ ```javascript
351
+ labels: {
352
+ loading: 'Caricamento dati…', // segnaposto di caricamento (visually hidden)
353
+ searchPlaceholder: 'Cerca...', // placeholder dell'input di ricerca
354
+ searchTitle: 'Cerca nella tabella', // attributo title dell'input
355
+ searchAriaLabel: 'Filtra risultati', // aria-label dell'input
356
+ info: 'Stai visualizzando le righe da {start} a {end}, su un totale di {filteredRec} record trovati',
357
+ // template del testo info (vedi `infoText`)
358
+ noRows: 'Nessun record trovato', // testo info e cella unica con set di dati vuoto
359
+ noResults: 'Nessun risultato per la ricerca', // idem, con ricerca senza risultati
360
+ sortAsc: 'Ordina questa colonna in senso ascendente (A → Z)', // title/aria-label del
361
+ sortDesc: 'Ordina questa colonna in senso discendente (Z → A)', // pulsante di ordinamento:
362
+ sortNone: 'Rimuovi l’ordinamento a questa colonna', // descrivono l'azione del
363
+ // prossimo click
364
+ paginationAriaLabel: 'Navigazione pagine', // aria-label del <nav>
365
+ prevPage: 'Pagina precedente', // title/aria-label del pulsante precedente
366
+ nextPage: 'Pagina successiva', // idem, successivo
367
+ pageTitle: 'Vai a pagina {page}', // idem, pulsanti pagina
368
+ currentPage: 'Pagina {page}, corrente' // idem, pagina corrente
369
+ }
370
+ ```
371
+
372
+ I segnaposto di `labels.info` / `infoText`:
373
+
374
+ | Segnaposto | Valore |
375
+ |---|---|
376
+ | `{start}` | indice (da 1) del primo record della pagina corrente, nel set filtrato |
377
+ | `{end}` | indice dell'ultimo record della pagina corrente |
378
+ | `{totRec}` | totale dei record non filtrati (dal JSON, vedi `totRecField`, o lunghezza dei dati) |
379
+ | `{filteredRec}` | totale dei record dopo il filtro |
380
+ | `{page}` | pagina corrente |
381
+ | `{totPages}` | numero di pagine |
382
+
383
+ I numeri vengono formattati secondo `locale`.
384
+
385
+ ## Colonne – `cols`
386
+
387
+ Parametro **obbligatorio**: array di oggetti, uno per colonna:
388
+
389
+ ```javascript
390
+ cols: [
391
+ {
392
+ key, // string – OBBLIGATORIO: chiave dell'oggetto riga (ammessa la notazione con
393
+ // punto per valori annidati, es. 'owner.name')
394
+ title, // contenuto del <th>: testo, HTML, Node, array domBuilder o funzione.
395
+ // Nelle colonne ordinabili è il testo del pulsante. Default: la chiave
396
+ dataType, // string – tipo di dato, una delle chiavi di `dataTypes`; `type` è un alias
397
+ // equivalente. Default: 'string'
398
+ render, // (row, tr, td) => contenuto | stringa mustache-like – vedi sotto. Default: null
399
+ tfootRender, // (rows, td) => contenuto | aggregato '@…' | stringa statica – contenuto della
400
+ // cella del tfoot (solo se `tfoot` è true). null = cella vuota. Default: null
401
+ rowHeading, // boolean – se true la cella è un'intestazione di riga (th[scope=row]).
402
+ // Default: false
403
+ searchable, // boolean – abilita la ricerca sulla colonna. Default: true
404
+ sortable, // boolean – abilita l'ordinamento sulla colonna. Default: true
405
+ sortValue, // valore | row => valore – valore usato per l'ordinamento; prevale su quello
406
+ // del tipo di dato. Default: non impostato (tipo di dato / valore grezzo)
407
+ searchValue, // idem, per la ricerca. Default: non impostato
408
+ condition, // boolean | params => boolean – se false la colonna non viene renderizzata.
409
+ // Default: true
410
+ headerClass, // string – classi del <th>, in sostituzione di quelle del tipo di dato
411
+ cellClass // string – classi delle celle body/tfoot, in sostituzione di quelle del tipo.
412
+ // Se solo una tra `headerClass` e `cellClass` è presente, l'altra assume lo
413
+ // stesso valore. Default: null (classi del tipo di dato)
414
+ },
415
+ ...
416
+ ]
417
+ ```
418
+
419
+ ### `render`
420
+
421
+ Funzione con argomenti posizionali, tutti facoltativi:
422
+
423
+ ```javascript
424
+ render: (row, tr, td) => contenuto
425
+ ```
426
+
427
+ - `row` – l'oggetto dati della riga
428
+ - `tr` – l'elemento `<tr>` della riga
429
+ - `td` – l'elemento della cella (`<td>` o `<th scope="row">`)
430
+
431
+ Il valore restituito è il contenuto della cella: testo, stringa HTML, numero, Node o array
432
+ domBuilder. Casi particolari:
433
+
434
+ - `undefined` (nessun `return`): la cella viene renderizzata dal tipo di dato, come se `render`
435
+ non fosse definito. Utile per decorare soltanto `td`/`tr` (classi, attributi, `title`):
436
+
437
+ ```javascript
438
+ render: (row, tr, td) => { if (row.amount < 0) td.classList.add('text-danger'); }
439
+ ```
440
+
441
+ - `null`: viene mostrato `renderNullAs`
442
+
443
+ Da attributo HTML (o da script, per i casi semplici) `render` può essere una stringa
444
+ mustache-like: ogni segnaposto `[[key]]` viene sostituito con il valore corrispondente della
445
+ riga (chiavi annidate ammesse; i valori null diventano `renderNullAs`):
446
+
447
+ ```javascript
448
+ render: '<a href="/users/[[id]]">[[owner.name]]</a>'
449
+ ```
450
+
451
+ Le stringhe HTML vengono inserite tramite la Sanitizer API (`Element.setHTML`, dove supportata):
452
+ script, attributi handler di eventi e URL `javascript:` vengono rimossi, mentre `class`, `id`,
453
+ `style` e `data-*` sono conservati.
454
+
455
+ ### `tfootRender`
201
456
 
202
- ### In sviluppo
457
+ Con `tfoot: true` ogni colonna produce una cella nel footer, il cui contenuto è dato da
458
+ `tfootRender`:
203
459
 
204
- `cols`, data-types, `locale` e opzioni di formattazione, icone (booleani e ordinamento),
205
- ordinamento, ricerca, paginazione, `trCallback`, `footerRender`, `refs`: vedi il TODO del componente.
460
+ - funzione `(rows, td) => contenuto`, dove `rows` è l'intero set filtrato (oppure i soli record
461
+ della pagina corrente se `updateFooterOnPageChange` è true)
462
+ - uno degli aggregati predefiniti (vedi sotto)
463
+ - qualsiasi altra stringa: contenuto statico (es. l'etichetta `'Totale'`)
464
+ - `null`: cella vuota
465
+
466
+ Aggregati predefiniti, calcolati sui valori **numerici** della colonna (i valori non numerici
467
+ vengono ignorati) e, tranne `@count`, formattati dal tipo di dato della colonna (es. come valuta
468
+ per `euro`, con `%` per `perc`):
469
+
470
+ | Aggregato | Valore |
471
+ |---|---|
472
+ | `'@sum'` | somma |
473
+ | `'@avg'` | media aritmetica |
474
+ | `'@min'` | minimo |
475
+ | `'@max'` | massimo |
476
+ | `'@count'` | numero di valori numerici (formattato con `locale`, senza il tipo di dato) |
477
+
478
+ Un aggregato non riconosciuto viene segnalato in console e produce una cella vuota. Le celle del
479
+ footer ricevono le stesse classi (`cellClass`) delle celle del body.
480
+
481
+ Il `tfoot` non è disponibile in modalità server-side (il componente riceve solo la pagina
482
+ corrente, gli aggregati sarebbero parziali): se richiesto viene ignorato con un avviso in console.
483
+
484
+ ## Tipi di dato – `dataTypes`
485
+
486
+ Ogni colonna ha un tipo di dato (`dataType`, default `string`) che ne definisce classi, rendering
487
+ e valori di ordinamento/ricerca. Tipi predefiniti:
488
+
489
+ | Tipo | Descrizione | Classi delle celle |
490
+ |---|---|---|
491
+ | `string` | valore così com'è | – |
492
+ | `num` | numero formattato con `numbersLocaleOpts` | `textEnd numeric nowrap` |
493
+ | `id` | id numerico: valore grezzo, allineato a destra, non ricercabile | `textEnd numeric` |
494
+ | `perc` | percentuale in scala 0–100, `currPercLocaleOpts` + `%` | `textEnd numeric nowrap` |
495
+ | `percDecimal` | percentuale in scala 0–1 (moltiplicata per 100) | `textEnd numeric nowrap` |
496
+ | `currency` | formato valuta `Intl` con `currency` | `textEnd numeric nowrap` |
497
+ | `euro` | come `currency`, EUR forzato | `textEnd numeric nowrap` |
498
+ | `date` | elemento `<time>`, `datesLocaleOpts`; accetta stringhe ISO, timestamp, `Date` e oggetti data Symfony | `textEnd nowrap` |
499
+ | `datetime` | come `date`, con la parte ora (`timesLocaleOpts`): `<time><span class="[nowrap]">data</span> <small>ora</small></time>` | `textEnd nowrap` |
500
+ | `bool` | icone `boolTrueIcon`/`boolFalseIcon` (accetta anche `1`/`0`, `'true'`/`'false'`); `null` → `renderNullAs`; non ordinabile né ricercabile | stili interni delle icone |
501
+ | `email` | andate a capo facoltative attorno alla `@` | – |
502
+
503
+ I tipi numerici applicano `renderNaNAs` ai valori non numerici e `renderZeroAs` (se impostato)
504
+ agli zeri. Per tutti i tipi i valori `null`/`undefined` vengono resi come `renderNullAs`.
505
+
506
+ ### Tipi personalizzati
507
+
508
+ Il parametro `dataTypes` è unito ai tipi predefiniti: una chiave già esistente sovrascrive solo
509
+ le proprietà indicate, una chiave nuova può estendere un tipo esistente con `inheritsFrom`.
510
+
511
+ ```javascript
512
+ dataTypes: {
513
+ // modifica solo le classi del tipo predefinito `num`
514
+ num: { cellClass: 'text-end fw-bold' },
515
+
516
+ // nuovo tipo basato su `num` (stesse classi, ordinamento, ricerca e gestione NaN/zero)
517
+ km: {
518
+ inheritsFrom: 'num',
519
+ render: (value, row, params) => `${value.toLocaleString(params.locale)} km`
520
+ }
521
+ }
522
+ ```
523
+
524
+ Proprietà di un tipo di dato (tutte facoltative):
525
+
526
+ ```javascript
527
+ {
528
+ headerClass, // string – classi di default del <th>
529
+ cellClass, // string – classi di default delle celle
530
+ internalCellClass, // (value, row, params) => string – classi aggiunte sempre alle celle,
531
+ // indipendentemente da `cellClass` (usato dal tipo `bool` per le icone)
532
+ render, // (value, row, params) => contenuto – non invocato per i valori null
533
+ sortValue, // (value, row, params) => valore per l'ordinamento
534
+ searchValue, // (value, row, params) => stringa per la ricerca
535
+ colDefaults, // Object – default forzati per le colonne di questo tipo
536
+ // (es. { sortable: false }), comunque sovrascrivibili nella colonna
537
+ inheritsFrom // string – solo tipi nuovi: tipo predefinito da estendere
538
+ }
539
+ ```
540
+
541
+ A differenza del `render` di colonna (`(row, tr, td)`), il `render` del tipo di dato riceve per
542
+ primo il **valore** della cella, perché il tipo è generico rispetto al campo.
543
+
544
+ ## Ordinamento
545
+
546
+ Ogni colonna con `sortable: true` (default; i tipi `bool` lo disattivano) ha un pulsante nel
547
+ `<th>` che cicla tra nessun ordinamento → ascendente → discendente → nessuno; l'attributo
548
+ `aria-sort` del `<th>` e l'icona riflettono lo stato, `title`/`aria-label` del pulsante
549
+ descrivono l'azione del click successivo (`labels.sortAsc/sortDesc/sortNone`). È attivo un solo
550
+ ordinamento per volta; il cambio di ordinamento riporta alla prima pagina.
551
+
552
+ Lato client il confronto usa i valori precalcolati per ogni riga (`sortValue` di colonna, poi del
553
+ tipo di dato, poi il valore grezzo): i valori vuoti (`null`, `''`, `NaN`) vanno sempre in coda,
554
+ i numeri sono confrontati numericamente, il resto con `localeCompare` (`locale`, collazione
555
+ numerica, insensibile a maiuscole e accenti). L'ordinamento è stabile.
556
+
557
+ `initialSort: { key, dir }` applica un ordinamento al caricamento; `setSort(key, dir)` lo cambia
558
+ via script.
559
+
560
+ ## Ricerca
561
+
562
+ L'input di ricerca (`search: true`) filtra le righe dopo `searchDebounce` ms dall'ultimo tasto:
563
+ ogni parola del termine (separatore: spazi) deve essere contenuta nel testo di ricerca della riga,
564
+ composto dai valori delle colonne `searchable` (`searchValue` di colonna, poi del tipo di dato,
565
+ poi il valore grezzo), senza distinzione tra maiuscole e minuscole. La ricerca riporta alla prima
566
+ pagina; `setSearch(term)` la imposta via script.
567
+
568
+ ## Paginazione
569
+
570
+ Con `perPage` > 0 (default 25) le righe vengono suddivise in pagine e sotto la tabella compare la
571
+ navigazione: pulsanti precedente/successivo (icone `paginationPrevIcon`/`paginationNextIcon`),
572
+ la prima e l'ultima pagina e `paginationDelta` pagine per lato della corrente, con puntini di
573
+ sospensione per le pagine omesse. La navigazione è nascosta quando c'è una sola pagina.
574
+ `perPage: 0` disattiva la paginazione (tutte le righe in un'unica pagina).
575
+
576
+ `goToPage(page)` cambia pagina via script; `state.page`/`state.totPages` riportano lo stato.
577
+
578
+ ### Caption e paginazione
579
+
580
+ La caption è mostrata **sotto la tabella**, nella barra `tableFooter`, allineata a sinistra; a
581
+ destra, nella stessa barra, la navigazione delle pagine. La caption non è un elemento
582
+ `<caption>` (che dovrebbe stare dentro la tabella e non potrebbe ospitare la navigazione senza
583
+ farla leggere come nome della tabella) ma un contenitore con id, collegato alla tabella tramite
584
+ `aria-labelledby`: il nome accessibile della tabella è quindi lo stesso di una `<caption>`.
585
+ Posizioni diverse si ottengono con gli slot `caption` e `pagination` del [template](#template).
586
+
587
+ ## Modalità server-side
588
+
589
+ Con `serverSide: true` il componente non elabora i dati in locale: a ogni cambio pagina,
590
+ ordinamento o ricerca invia una nuova richiesta GET a `jsonUrl`, aggiungendo alla query string i
591
+ parametri definiti in `serverParams` (quelli già presenti in `jsonUrl` vengono conservati):
592
+
593
+ ```javascript
594
+ serverParams: {
595
+ page: 'page', // pagina richiesta (da 1)
596
+ start: 'start', // indice (da 0) del primo record richiesto = (page - 1) * perPage,
597
+ // comodo per `LIMIT start, perPage`
598
+ perPage: 'perPage', // record per pagina
599
+ sort: 'sort', // chiave della colonna ordinata (omesso senza ordinamento)
600
+ dir: 'dir', // 'asc' | 'desc' (omesso senza ordinamento)
601
+ search: 'search' // termine di ricerca (omesso se vuoto)
602
+ }
603
+ ```
604
+
605
+ Un valore `null` omette il parametro (es. `serverParams: { start: null }` per inviare solo `page`).
606
+ Richiesta di esempio, seconda pagina ordinata per nome con ricerca attiva:
607
+
608
+ ```
609
+ GET /api/rows.json?page=2&start=25&perPage=25&sort=name&dir=desc&search=mar
610
+ ```
611
+
612
+ Il JSON di risposta deve contenere le **sole righe della pagina richiesta** in `jsonDataField`,
613
+ il totale dei record in `totRecField` e il numero di record che soddisfano la ricerca in
614
+ `filteredRecField` (se assente vale il totale):
615
+
616
+ ```json
617
+ { "data": [ ...25 righe... ], "totRec": 1500, "filteredRec": 40 }
618
+ ```
619
+
620
+ Durante la richiesta il contenitore principale ha `aria-busy="true"` (la tabella viene resa
621
+ semitrasparente); le risposte superate da una richiesta più recente vengono ignorate. Se la
622
+ pagina richiesta non esiste più (il set di dati si è ridotto), viene richiesta l'ultima
623
+ disponibile. In questa modalità `data`, `filtered` e `pageRows` dello stato contengono solo la
624
+ pagina corrente e il `tfoot` non è disponibile. `serverSide` richiede `jsonUrl`: con `data` inline
625
+ viene ignorato con un avviso in console.
626
+
627
+ ## Template
628
+
629
+ `template` definisce il contenuto del contenitore principale come array domBuilder, in cui gli
630
+ elementi `{ slot: 'nome' }` vengono sostituiti dalle parti predefinite:
631
+
632
+ | Slot | Contenuto |
633
+ |---|---|
634
+ | `infoSection` | sezione info completa (testo info + input di ricerca, in riga) |
635
+ | `resultInfo` | solo il testo info |
636
+ | `search` | solo l'input di ricerca (se `search` è true) |
637
+ | `table` | wrapper + tabella (thead, tbody, tfoot) e barra `tableFooter` con caption e paginazione |
638
+ | `caption` | solo la caption (se `caption` è impostata) |
639
+ | `pagination` | solo la navigazione delle pagine (se `perPage` > 0) |
640
+
641
+ `caption` e `pagination` fanno parte della barra sotto la tabella dello slot `table`; se il
642
+ template li colloca esplicitamente altrove, vengono rimossi dalla barra (che sparisce se resta
643
+ vuota).
644
+
645
+ Il default `[{ slot: 'infoSection' }, { slot: 'table' }]` corrisponde alla struttura descritta
646
+ in [Struttura generata](#struttura-generata). Esempio con ricerca e paginazione sopra la tabella
647
+ e info sotto:
648
+
649
+ ```javascript
650
+ template: [
651
+ { className: 'flex gap-2 mbe-sm', children: [{ slot: 'search' }, { slot: 'pagination' }] },
652
+ { slot: 'table' }, // barra sotto la tabella con la sola caption
653
+ { slot: 'resultInfo' }
654
+ ]
655
+ ```
656
+
657
+ Da `init()` è accettata anche una funzione `(parts, params) => array`, dove `parts` contiene le
658
+ parti predefinite (oggetti domBuilder) con le stesse chiavi degli slot. Gli slot non
659
+ riconosciuti vengono segnalati in console e ignorati.
206
660
 
207
661
  ## Struttura generata
208
662
 
209
663
  ```html
210
664
  <json-table>
211
- <section class="[wrapper] [mainWrapperExtraClass]">
212
- <div class="[infoOuter] [outerInfoExtraClass]">
213
- <div class="[info] [infoExtraClass]">
214
- <div class="[resultInfo]" aria-live="polite">Visualizzate <strong>N</strong> righe su <strong>M</strong></div>
215
- <div class="[search]"> <!-- solo se search: true -->
216
- <input type="search" class="form-control" ...>
665
+ <section class="[wrapper] [classes.wrapper]">
666
+ <div class="[infoOuter] [classes.infoOuter]">
667
+ <div class="[info] [classes.info]">
668
+ <div class="[resultInfo] [classes.resultInfo]" aria-live="polite">Stai visualizzando le righe da 1 a 25, ...</div>
669
+ <div class="[search] [classes.search]"> <!-- solo se search: true -->
670
+ <input type="search" class="form-control form-control-sm" ...>
217
671
  </div>
218
672
  </div>
219
673
  </div>
220
- <div class="table-responsive"> <!-- tableWrapperClass -->
221
- <table class="[table] table table-bordered"> <!-- tableClass -->
222
- <caption>...</caption> <!-- solo se caption -->
223
- <!-- thead / tbody / tfoot: in sviluppo -->
224
- </table>
674
+ <div class="[tableOuter]">
675
+ <div class="[tableWrapper] table-responsive"> <!-- classes.tableWrapper -->
676
+ <table class="[table] table table-bordered" aria-labelledby="jt-1-caption"> <!-- classes.table; aria-labelledby solo se caption -->
677
+ <thead>
678
+ <tr>
679
+ <th scope="col" data-key="name" data-sortable="true" class="[sortable] ..." aria-sort="ascending">
680
+ <button type="button" class="[sortBtn] btn-reset" title="Ordina ..." aria-label="Nome: Ordina ...">
681
+ <span class="[sortTitle]">Nome</span>
682
+ <span class="[sortIcon]" aria-hidden="true"><svg ...></svg></span>
683
+ </button>
684
+ </th>
685
+ <th scope="col" data-key="active" class="text-center">Attivo</th> <!-- non ordinabile -->
686
+ </tr>
687
+ </thead>
688
+ <tbody>
689
+ <tr data-jt-idx="0">
690
+ <th scope="row" data-key="id" class="text-end text-numeric">1</th> <!-- rowHeading -->
691
+ <td data-key="name">Mario</td>
692
+ <td data-key="active" class="[boolCell] [boolTrue]"><svg ...></svg></td>
693
+ </tr>
694
+ <!-- oppure, senza righe: -->
695
+ <tr><td colspan="3" class="[empty] [classes.empty]">Nessun record trovato</td></tr>
696
+ </tbody>
697
+ <tfoot> <!-- solo se tfoot: true -->
698
+ <tr><td data-key="id"></td><td data-key="name">Totale</td>...</tr>
699
+ </tfoot>
700
+ </table>
701
+ </div>
702
+ <div class="[tableFooter] [classes.tableFooter]"> <!-- solo se caption o perPage > 0 -->
703
+ <div class="[caption] [classes.caption]" id="jt-1-caption">Utenti</div> <!-- solo se caption -->
704
+ <nav class="[pagination] [classes.pagination]" aria-label="Navigazione pagine"> <!-- solo se perPage > 0; hidden con una sola pagina -->
705
+ <ul class="[paginationList]">
706
+ <li class="[paginationItem]"><button type="button" class="[paginationBtn] btn-reset [paginationArrow]" data-page="prev" aria-label="Pagina precedente" disabled><svg ...></svg></button></li>
707
+ <li class="[paginationItem]"><button type="button" class="[paginationBtn] btn-reset" data-page="1" aria-current="page" aria-label="Pagina 1, corrente">1</button></li>
708
+ <li class="[paginationItem]"><button type="button" class="[paginationBtn] btn-reset" data-page="2" aria-label="Vai a pagina 2">2</button></li>
709
+ <li class="[paginationItem]"><span class="[paginationEllipsis]" aria-hidden="true">…</span></li>
710
+ <li class="[paginationItem]"><button type="button" class="[paginationBtn] btn-reset" data-page="10" aria-label="Vai a pagina 10">10</button></li>
711
+ <li class="[paginationItem]"><button type="button" class="[paginationBtn] btn-reset [paginationArrow]" data-page="next" aria-label="Pagina successiva"><svg ...></svg></button></li>
712
+ </ul>
713
+ </nav>
714
+ </div>
225
715
  </div>
226
716
  </section>
227
717
  </json-table>
228
718
  ```
229
719
 
230
- Le classi tra parentesi quadre sono quelle interne del CSS module.
720
+ Le classi tra parentesi quadre sono quelle interne del CSS module. Nei `<th>` ordinabili
721
+ l'attributo `aria-sort` (`ascending`/`descending`) è presente solo sulla colonna ordinata;
722
+ `title` e `aria-label` del pulsante descrivono l'azione del click successivo.
231
723
 
232
724
  ## Custom properties CSS
233
725
 
@@ -236,8 +728,37 @@ Definite in `json-table.minimo.tokens.mjs` (namespace `--jt-*`):
236
728
  | Proprietà | Default (token minimo) |
237
729
  |---|---|
238
730
  | `--jt-wrapper-margin-block` | `{size.md}` |
731
+ | `--jt-busy-opacity` | `.5` |
239
732
  | `--jt-info-gap` | `{size.sm}` |
240
733
  | `--jt-info-font-size` | `{font.size.sm}` |
241
734
  | `--jt-info-color` | `{text.muted}` |
242
735
  | `--jt-info-outer-padding-block-end` | `{size.xs}` |
243
736
  | `--jt-search-max-width` | `20rem` |
737
+ | `--jt-thead-hover-background-color` | `color-mix(in srgb, {table.thead.background.color} 90%, {accent})` |
738
+ | `--jt-sort-btn-padding-block` | `{table.cell.padding.block}` |
739
+ | `--jt-sort-btn-padding-inline` | `{table.cell.padding.inline}` |
740
+ | `--jt-sort-btn-line-height` | `{table.cell.line-height}` |
741
+ | `--jt-sort-icon-size` | `1em` |
742
+ | `--jt-sort-icon-gap` | `.4em` |
743
+ | `--jt-sort-icon-none-opacity` | `.4` |
744
+ | `--jt-table-footer-padding-block-start` | `{size.xs}` |
745
+ | `--jt-caption-font-size` | `{font.size.sm}` |
746
+ | `--jt-caption-color` | `{text.muted}` |
747
+ | `--jt-pagination-border-width` | `1px` |
748
+ | `--jt-pagination-border-color` | `{table.border.color}` |
749
+ | `--jt-pagination-radius` | `{radius.xxs}` |
750
+ | `--jt-pagination-font-size` | `{font.size.sm}` |
751
+ | `--jt-pagination-line-height` | `{table.cell.line-height}` |
752
+ | `--jt-pagination-btn-min-width` | `2.2em` |
753
+ | `--jt-pagination-btn-padding-block` | `{table.cell.padding.block}` |
754
+ | `--jt-pagination-btn-padding-inline` | `{table.cell.padding.inline}` |
755
+ | `--jt-pagination-icon-size` | `1em` |
756
+ | `--jt-pagination-current-background-color` | `{table.thead.background.color}` |
757
+ | `--jt-pagination-current-font-weight` | `{font.weight.semibold}` |
758
+ | `--jt-pagination-hover-background-color` | `{table.body.tr.hover-bg-color}` |
759
+ | `--jt-pagination-disabled-opacity` | `.5` |
760
+ | `--jt-bool-icon-size` | `1.1em` |
761
+ | `--jt-bool-true-color` | `{status.success.color}` |
762
+ | `--jt-bool-false-color` | `{status.danger.color}` |
763
+ | `--jt-empty-color` | `{text.muted}` |
764
+ | `--jt-empty-padding-block` | `{size.sm}` |