@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.
@@ -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.28.4",
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.14",
96
- "@massimo-cassandro/stylelint-config": "^2.3.14",
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"
@@ -1,3 +1,4 @@
1
+ /* TODO[epic=v2] eliminare custom props --_*, tutte le props devono essere gestibili dalle props globali */
1
2
 
2
3
  /* TODO rivedere tutto */
3
4
 
package/src/css/flex.css CHANGED
@@ -1,3 +1,5 @@
1
+ /* TODO[epic=v2] unificare regole comuni flex e grid */
2
+
1
3
  .flex {
2
4
  display: flex;
3
5
  flex-flow: row wrap;
package/src/css/grid.css CHANGED
@@ -1,4 +1,4 @@
1
- /* TODO unificare regole comuni flex e grid */
1
+ /* TODO[epic=v2] unificare regole comuni flex e grid */
2
2
 
3
3
  .grid {
4
4
  --_cols: 1;
@@ -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