@massimo-cassandro/minimo 1.28.4 → 1.29.0
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/dev-tools/starter-kit/templates/CLAUDE-frontend.md +1 -0
- package/dev-tools/update-version/index.mjs +1 -1
- package/package.json +5 -4
- package/src/css/buttons/status-buttons.css +1 -0
- package/src/css/flex.css +2 -0
- package/src/css/grid.css +1 -1
- package/src/web-components/s-datatable-component/docs.md +56 -0
- package/src/web-components/s-datatable-component/s-datatable-component.js +134 -0
|
@@ -11,3 +11,4 @@
|
|
|
11
11
|
- In linea di massima non proporre soluzioni che richiedano modifiche alla parte back-end, se presente, a meno che non sia esplicitamente richiesto
|
|
12
12
|
- nella chat evitare neologismi come parsare, committare ecc., usare i termini corrispondenti in italiano o il termine originale in inglese dove non sia possibile un equivalente in italiano. Non coniugare mai secondo le regole dell'italiano verbi di altre lingue
|
|
13
13
|
- nei commenti su più righe preferire `/* ... */` piuttosto che ripetere `//` ad igni inizio riga
|
|
14
|
+
- a meno che non sia esplicitamente richiesto di eseguire subito una modifica, NON ESEGUIRE MAI nulla autonomamente ma mostra sempre un piano d'azione prima di procedere
|
|
@@ -78,7 +78,7 @@ async function run() {
|
|
|
78
78
|
('v.' + params.newSemver) +
|
|
79
79
|
(params.logRow.descr
|
|
80
80
|
? ' - ' + params.logRow.descr
|
|
81
|
-
: params.updateMode === 'patch'
|
|
81
|
+
: params.updateMode === 'patch' && !(params.noLogV0 && params.semverArray[0] === 0)
|
|
82
82
|
? ' - Fix / Dependencies upd'
|
|
83
83
|
: '')
|
|
84
84
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massimo-cassandro/minimo",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.0",
|
|
4
4
|
"description": "Light css framework & utilities",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.js",
|
|
@@ -44,7 +44,8 @@
|
|
|
44
44
|
"test sideEffects tree-shaking": "node ./test/sideEffects-tree-shaking/run-test.mjs",
|
|
45
45
|
"checkJS": "npx tsc --noEmit -p jsconfig.json",
|
|
46
46
|
"checkJS ./src": "npx tsc --noEmit --allowJs --checkJs --target ES2023 --module ESNext --allowSyntheticDefaultImports --moduleResolution bundler declarations.d.ts types/global.d.ts src/**/*.js",
|
|
47
|
-
"check outdated": "echo '*** ROOT ***' && npm outdated --omit=peer && echo '*** DEMO ***' && npm --prefix ./demo outdated && echo '*** END ***'"
|
|
47
|
+
"check outdated": "echo '*** ROOT ***' && npm outdated --omit=peer && echo '*** DEMO ***' && npm --prefix ./demo outdated && echo '*** END ***'",
|
|
48
|
+
"npm upd": "echo '*** ROOT ***' && npm update && echo '*** DEMO ***' && npm --prefix ./demo update && echo '*** END ***'"
|
|
48
49
|
},
|
|
49
50
|
"license": "MIT",
|
|
50
51
|
"author": "Massimo Cassandro",
|
|
@@ -92,8 +93,8 @@
|
|
|
92
93
|
}
|
|
93
94
|
},
|
|
94
95
|
"devDependencies": {
|
|
95
|
-
"@massimo-cassandro/eslint-config": "^3.0.
|
|
96
|
-
"@massimo-cassandro/stylelint-config": "^2.3.
|
|
96
|
+
"@massimo-cassandro/eslint-config": "^3.0.16",
|
|
97
|
+
"@massimo-cassandro/stylelint-config": "^2.3.15",
|
|
97
98
|
"@types/node": "^26.2.0",
|
|
98
99
|
"jsdom": "^29.1.1",
|
|
99
100
|
"style-dictionary": "^5.5.1"
|
package/src/css/flex.css
CHANGED
package/src/css/grid.css
CHANGED
|
@@ -272,6 +272,30 @@ cols: [
|
|
|
272
272
|
// return `<strong>${tot.toLocaleString('it-IT')} €</strong>`;
|
|
273
273
|
// }
|
|
274
274
|
|
|
275
|
+
_collapseKey, // Percorso (notazione punto) del campo da usare come chiave di
|
|
276
|
+
// raggruppamento per collassare valori ripetuti su righe consecutive.
|
|
277
|
+
// `true` = riusa _field. Colonne diverse con la stessa _collapseKey
|
|
278
|
+
// collassano insieme come un unico gruppo logico (es. più colonne
|
|
279
|
+
// che riportano dati della stessa "commessa" su più righe/attività).
|
|
280
|
+
//
|
|
281
|
+
// Opera sulle righe effettivamente renderizzate (già filtrate/ordinate/
|
|
282
|
+
// paginate): la prima riga di ogni pagina mostra sempre il valore pieno,
|
|
283
|
+
// anche se continua un gruppo iniziato nella pagina precedente.
|
|
284
|
+
// Si ricalcola automaticamente ad ogni cambio pagina, ordinamento,
|
|
285
|
+
// ricerca o multisearch.
|
|
286
|
+
|
|
287
|
+
_collapsePlaceholder, // Testo da mostrare al posto del valore ripetuto. Accetta lo stesso
|
|
288
|
+
// ventaglio di _render/_cellTitle: stringa semplice, stringa mustache-like
|
|
289
|
+
// con segnaposto [[chiave]] (interpolati sulla riga corrente), oppure
|
|
290
|
+
// funzione (row) => string. Default null (anche se la funzione ritorna
|
|
291
|
+
// null/undefined) = il testo della cella resta invariato (utile insieme
|
|
292
|
+
// alla sola _collapseClass, quando basta l'effetto visivo a segnalare
|
|
293
|
+
// il collasso).
|
|
294
|
+
|
|
295
|
+
_collapseClass, // Classe CSS da assegnare alla cella collassata. Nessuna classe di
|
|
296
|
+
// default: il trattamento visivo (es. attenuazione cromatica) è a
|
|
297
|
+
// discrezione del consumer nel proprio CSS applicativo.
|
|
298
|
+
|
|
275
299
|
// ── Proprietà native simple-datatables (senza prefisso _) ────────────
|
|
276
300
|
|
|
277
301
|
type, // Tipo di dato: 'string' | 'number' | 'date' | 'boolean' | 'html' | 'other'
|
|
@@ -359,6 +383,38 @@ non cambiano navigando tra le pagine e si aggiornano solo quando cambia la ricer
|
|
|
359
383
|
Con `updateFooterOnPageChange: true` la callback riceve i record della pagina corrente: utile per
|
|
360
384
|
subtotali di pagina; il footer si aggiorna sia al cambio filtro sia al cambio pagina.
|
|
361
385
|
|
|
386
|
+
### Esempio con collapse – valori ripetuti su righe consecutive
|
|
387
|
+
|
|
388
|
+
Utile quando ogni riga è un dettaglio (es. un'attività) ma alcune colonne riportano dati di un
|
|
389
|
+
gruppo più ampio (es. la commessa a cui l'attività appartiene), ripetuti identici su più righe
|
|
390
|
+
consecutive quando la tabella è ordinata per quel gruppo.
|
|
391
|
+
|
|
392
|
+
```javascript
|
|
393
|
+
const cols = [
|
|
394
|
+
{
|
|
395
|
+
_field: 'commessaId',
|
|
396
|
+
_heading: 'Commessa',
|
|
397
|
+
_collapseKey: true, // riusa _field ('commessaId') come chiave di gruppo
|
|
398
|
+
_collapsePlaceholder: '»', // mostrato al posto del valore ripetuto
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
_field: 'budgetCommessa',
|
|
402
|
+
_heading: 'Budget',
|
|
403
|
+
type: 'number',
|
|
404
|
+
_collapseKey: 'commessaId', // stessa chiave: collassa insieme alla colonna precedente
|
|
405
|
+
_collapseClass: 'text-muted', // classe CSS a discrezione del consumer
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
_field: 'attivita',
|
|
409
|
+
_heading: 'Attività', // colonna di dettaglio, mai collassata
|
|
410
|
+
},
|
|
411
|
+
];
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
La prima riga di ogni pagina mostra sempre il valore pieno, anche se continua un gruppo iniziato
|
|
415
|
+
nella pagina precedente. Il calcolo si ricalcola automaticamente ad ogni cambio pagina, ordinamento,
|
|
416
|
+
ricerca o multisearch (opera sulle righe effettivamente renderizzate, non sui dati grezzi).
|
|
417
|
+
|
|
362
418
|
## Testo info
|
|
363
419
|
|
|
364
420
|
Il componente sovrascrive il testo dell'area informativa (in basso a sinistra) per includere
|
|
@@ -59,6 +59,24 @@ import caretRightIcon from '../../icons/caret-right.svg?inline';
|
|
|
59
59
|
* Il <tfoot> viene aggiunto solo se almeno una colonna lo definisce.
|
|
60
60
|
* La cella eredita le classi CSS (allineamento) della colonna.
|
|
61
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
|
+
*
|
|
62
80
|
* @property {string|Function} [_sortValue] Percorso del campo da usare per l'ORDINAMENTO al posto
|
|
63
81
|
* del contenuto visualizzato, oppure una funzione
|
|
64
82
|
* `(row) => string|number` per casi complessi
|
|
@@ -257,6 +275,17 @@ class SimpleDatatableAdapter extends HTMLElement {
|
|
|
257
275
|
*/
|
|
258
276
|
this._filteredData = null;
|
|
259
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
|
+
|
|
260
289
|
// Handler bound per poterli rimuovere in disconnectedCallback
|
|
261
290
|
this._onSearch = this._handleSearch.bind(this);
|
|
262
291
|
this._onMultiSearch = this._handleMultiSearch.bind(this);
|
|
@@ -688,6 +717,7 @@ class SimpleDatatableAdapter extends HTMLElement {
|
|
|
688
717
|
// Conserva i dati grezzi e le definizioni di colonna per il tfoot
|
|
689
718
|
this._rawData = rawData;
|
|
690
719
|
this._footerCols = cols;
|
|
720
|
+
this._collapseOrigCache.clear(); // i jidx ripartono da zero, la cache precedente non è più valida
|
|
691
721
|
|
|
692
722
|
this.columns = cols.map((col_settings, idx) => {
|
|
693
723
|
col_settings = parseCols(col_settings);
|
|
@@ -887,13 +917,24 @@ class SimpleDatatableAdapter extends HTMLElement {
|
|
|
887
917
|
// ── Tfoot ─────────────────────────────────────────────────────────────
|
|
888
918
|
// Eseguito sempre, indipendentemente dalla presenza della paginazione.
|
|
889
919
|
this._renderTfoot();
|
|
920
|
+
|
|
921
|
+
// ── Collapse valori ripetuti ────────────────────────────────────────
|
|
922
|
+
this._applyCollapse();
|
|
890
923
|
});
|
|
891
924
|
|
|
892
925
|
// Salva la pagina corrente nel cookie ad ogni cambio di pagina;
|
|
893
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.
|
|
894
929
|
this._dt.on('datatable.page', page => {
|
|
895
930
|
this._savePageCookie(page);
|
|
896
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();
|
|
897
938
|
});
|
|
898
939
|
|
|
899
940
|
// Aggiorna il tfoot ad ogni cambio di filtro (ricerca / multisearch).
|
|
@@ -928,6 +969,7 @@ class SimpleDatatableAdapter extends HTMLElement {
|
|
|
928
969
|
|
|
929
970
|
this._filteredCount = this._filteredData?.length ?? rawData.length;
|
|
930
971
|
this._renderTfoot();
|
|
972
|
+
this._applyCollapse();
|
|
931
973
|
};
|
|
932
974
|
this._dt.on('datatable.search', onFilter);
|
|
933
975
|
this._dt.on('datatable.multisearch', onFilter);
|
|
@@ -1035,6 +1077,98 @@ class SimpleDatatableAdapter extends HTMLElement {
|
|
|
1035
1077
|
}
|
|
1036
1078
|
|
|
1037
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
|
+
|
|
1038
1172
|
} // end component
|
|
1039
1173
|
|
|
1040
1174
|
|