@massimo-cassandro/minimo 0.16.0 → 0.16.2

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.
@@ -20,6 +20,26 @@
20
20
  }
21
21
  }
22
22
  },
23
+ "tfoot": {
24
+ "background": {
25
+ "color": {
26
+ "$type": "color",
27
+ "$value": "{table.thead.background.color}"
28
+ }
29
+ },
30
+ "border": {
31
+ "width": {
32
+ "$type": "dimension",
33
+ "$value": "{table.thead.border.width}"
34
+ }
35
+ },
36
+ "font": {
37
+ "weight": {
38
+ "$type": "fontWeight",
39
+ "$value": "{font.weight.semibold}"
40
+ }
41
+ }
42
+ },
23
43
  "body": {
24
44
  "border": {
25
45
  "width": {
@@ -0,0 +1,86 @@
1
+ export default {
2
+ table: {
3
+ thead: {
4
+ background: {
5
+ color: {
6
+ $type: 'color',
7
+ $value: '#ddd'
8
+ }
9
+ },
10
+ border: {
11
+ width : {
12
+ $type: 'dimension',
13
+ $value: '3px'
14
+ }
15
+ }
16
+ },
17
+ tfoot: {
18
+ background: {
19
+ color: {
20
+ $type: 'color',
21
+ $value: '{table.thead.background.color}'
22
+ }
23
+ },
24
+ border: {
25
+ width : {
26
+ $type: 'dimension',
27
+ $value: '{table.thead.border.width}'
28
+ }
29
+ },
30
+ font: {
31
+ weight: {
32
+ $type: 'fontWeight',
33
+ $value: '{font.weight.semibold}',
34
+ }
35
+ }
36
+ },
37
+
38
+ body: {
39
+ background: {
40
+ color: null
41
+ },
42
+ border: {
43
+ 'width' : {
44
+ $type: 'dimension',
45
+ $value: '1px'
46
+ }
47
+ },
48
+ tr: {
49
+ 'alt-bg-color': {
50
+ $type: 'color',
51
+ $value: '#efefef'
52
+ },
53
+ 'hover-bg-color': {
54
+ $type: 'color',
55
+ $value: '#cbcbcb'
56
+ }
57
+ }
58
+ },
59
+
60
+ border: {
61
+ 'color' : {
62
+ $type: 'color',
63
+ $value: '#333'
64
+ }
65
+ },
66
+
67
+ cell: {
68
+ 'padding' : {
69
+ block: {
70
+ $type: 'dimension',
71
+ $value: '.3rem'
72
+ },
73
+ inline: {
74
+ $type: 'dimension',
75
+ $value: '.6rem'
76
+ }
77
+ },
78
+ 'line-height': {
79
+ $type: 'number',
80
+ $value: '1.3'
81
+ }
82
+ }
83
+
84
+
85
+ }
86
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massimo-cassandro/minimo",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Light css framework & utilities",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/massimo-cassandro/minimo#readme",
package/src/css/table.css CHANGED
@@ -22,6 +22,7 @@
22
22
  tr {
23
23
  border-bottom: var(--_border);
24
24
  }
25
+
25
26
  thead {
26
27
  background-color: var(--table-thead-background-color);
27
28
  tr:last-child {
@@ -29,6 +30,16 @@
29
30
  }
30
31
  }
31
32
 
33
+ tfoot {
34
+ background-color: var(--table-tfoot-background-color);
35
+ tr:first-child {
36
+ border-block-start: var(--table-tfoot-border-width) solid var(--_border-color);
37
+ }
38
+ th, td {
39
+ font-weight: var(--table-tfoot-font-weight);
40
+ }
41
+ }
42
+
32
43
  tbody {
33
44
  tr:nth-child(even) {
34
45
  background-color: var(--_alternate-row-bg);
@@ -276,6 +276,9 @@
276
276
  --table-cell-line-height: 1.3;
277
277
  --table-cell-padding-block: .3rem;
278
278
  --table-cell-padding-inline: .6rem;
279
+ --table-tfoot-background-color: var(--table-thead-background-color);
280
+ --table-tfoot-border-width: var(--table-thead-border-width);
281
+ --table-tfoot-font-weight: var(--font-weight-semibold);
279
282
  --table-thead-background-color: #ddd;
280
283
  --table-thead-border-width: .1875rem;
281
284
  --text-color: #000;
@@ -354,28 +354,43 @@ Ricerca senza risultati:
354
354
 
355
355
  Il testo si aggiorna automaticamente ad ogni cambio pagina, ricerca e multisearch.
356
356
 
357
- ## Parametro `topSlot`
357
+ ## Parametri `topSlot` e `showInfoAtTop`
358
358
 
359
- Permette di inserire un elemento DOM o del markup HTML nell'area in alto a sinistra
360
- del datatable — lo spazio normalmente occupato dal selettore "righe per pagina"
361
- (qui disabilitato). È utile per aggiungere pulsanti contestuali, filtri aggiuntivi
362
- o qualsiasi controllo da affiancare alla search box.
359
+ Entrambi occupano l'area in alto a sinistra del datatable — lo spazio normalmente
360
+ riservato al selettore "righe per pagina" (qui disabilitato). I due parametri si
361
+ escludono: se `showInfoAtTop` è `true` (default), `topSlot` viene ignorato.
363
362
 
364
- L'elemento viene avvolto in un `<div class="datatable-top-slot">` e inserito
365
- come primo figlio dell'area superiore, prima della search box.
363
+ ### `showInfoAtTop`
366
364
 
367
- È configurabile solo via script (non da attributo HTML), perché accetta sia
368
- stringhe HTML che elementi DOM già costruiti.
365
+ Replica nell'area superiore lo stesso testo dell'area `.info` in basso,
366
+ mantenuto in sincronia ad ogni cambio pagina o filtro. Il nodo viene nascosto
367
+ automaticamente quando non ci sono record o la ricerca non produce risultati
368
+ (evita di mostrare "Nessun risultato…" in due posti).
369
+
370
+ ```javascript
371
+ el.init({
372
+ json: '/api/data.json',
373
+ cols: [...],
374
+ showInfoAtTop: true,
375
+ });
376
+ ```
377
+
378
+ ### `topSlot`
379
+
380
+ Inserisce un elemento DOM o markup HTML arbitrario, avvolto in un
381
+ `<div class="datatable-top-slot">`. Accetta sia una stringa HTML che un `Element`
382
+ già costruito (utile per mantenere un riferimento al nodo e aggiungervi listener).
383
+ Non è leggibile da attributo HTML, solo via `init()`.
369
384
 
370
385
  ```javascript
371
386
  // Stringa HTML
372
387
  el.init({
373
388
  json: '/api/data.json',
374
389
  cols: [...],
375
- topSlot: '<button class="btn btn-sm btn-primary" onclick="...">Esporta CSV</button>',
390
+ topSlot: '<button class="btn btn-sm btn-primary">Esporta CSV</button>',
376
391
  });
377
392
 
378
- // Elemento DOM (utile quando si vuole mantenere un riferimento al nodo)
393
+ // Elemento DOM
379
394
  const btn = document.createElement('button');
380
395
  btn.className = 'btn btn-sm btn-outline-secondary';
381
396
  btn.textContent = 'Nuova commessa';
@@ -114,8 +114,17 @@ import caretRightIcon from '../../icons/caret-right.svg?inline';
114
114
  * Accetta:
115
115
  * - `Element`: un nodo DOM già costruito
116
116
  * - stringa HTML: parsata via innerHTML
117
+ * Ignorato se `showInfoAtTop` è true.
117
118
  * Non è leggibile da attributo HTML (solo via script).
118
119
  *
120
+ * @param {boolean} showInfoAtTop Se true, mostra nell'area superiore (spazio top-sinistra)
121
+ * lo stesso testo dell'area `.info` in basso, mantenuto in
122
+ * sync ad ogni cambio pagina/filtro. Il nodo viene nascosto
123
+ * automaticamente quando non ci sono record o la ricerca
124
+ * non produce risultati. Ha precedenza su `topSlot`:
125
+ * se entrambi sono definiti, `topSlot` viene ignorato.
126
+ * Default: true.
127
+ *
119
128
  * @param {string[]} refs Elenco di percorsi URL da cui, se si proviene,
120
129
  * la pagina corrente viene ripristinata dal cookie
121
130
  * di sessione `sd-pag`. Se il referrer non è tra
@@ -404,8 +413,27 @@ class SimpleDatatableAdapter extends HTMLElement {
404
413
  // 2. Attributo HTML
405
414
  const attr = this.getAttribute(name);
406
415
  if (attr !== null) {
407
- // Auto-parse JSON per array e oggetti (es. cols='[…]')
416
+ // Auto-parse booleani quando il defaultValue è boolean e il valore
417
+ // è una delle stringhe canoniche true/false/0/1, oppure true/false.
418
+ // Casi supportati:
419
+ // showInfoAtTop (attributo senza valore → attr === '') → true
420
+ // showInfoAtTop="true" / showInfoAtTop="1" → true
421
+ // showInfoAtTop="false" / showInfoAtTop="0" → false
422
+ // Auto-parse booleani solo quando il parametro è dichiaratamente booleano
423
+ // (defaultValue === true | false). Copre tutti i formati HTML idiomatici:
424
+ // showInfoAtTop (attributo senza valore → '') → true
425
+ // showInfoAtTop="true" / showInfoAtTop="1" → true
426
+ // showInfoAtTop="false" / showInfoAtTop="0" → false
427
+ // I valori "1" e "0" NON vengono intercettati qui se defaultValue non è
428
+ // boolean, per non interferire con parametri numerici (es. perPage="1").
408
429
  const trimmed = attr.trim();
430
+ if (typeof defaultValue === 'boolean') {
431
+ const lower = trimmed.toLowerCase();
432
+ // Stringa vuota '' = attributo presente senza valore → true (standard HTML)
433
+ return lower !== 'false' && lower !== '0';
434
+ }
435
+
436
+ // Auto-parse JSON per array e oggetti (es. cols='[…]')
409
437
  if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
410
438
  try { return JSON.parse(trimmed); } catch { /* restituisce la stringa grezza */ }
411
439
  }
@@ -760,24 +788,25 @@ class SimpleDatatableAdapter extends HTMLElement {
760
788
  });
761
789
 
762
790
  this._dt.on('datatable.init', () => {
791
+ // ── Icone paginazione ─────────────────────────────────────────────────
763
792
  const pagination = this.querySelector(`.${styles.paginationList}`);
764
- if (!pagination) return;
765
-
766
- const items = pagination.querySelectorAll(`.${styles.paginationListItem}`);
767
- if (!items.length) return;
768
-
769
- pagination.querySelector(
770
- `.${styles.paginationListItem}:first-child .${styles.paginationListItemBtn}`
771
- ).innerHTML = caretLeftIcon;
772
-
773
- pagination.querySelector(
774
- `.${styles.paginationListItem}:last-child .${styles.paginationListItemBtn}`
775
- ).innerHTML = caretRightIcon;
776
-
777
- pagination.querySelectorAll(`.${styles.paginationListItemBtn}:has(svg)`)
778
- .forEach(btn => btn.classList.add(styles.hasSvg));
793
+ if (pagination) {
794
+ const items = pagination.querySelectorAll(`.${styles.paginationListItem}`);
795
+ if (items.length) {
796
+ pagination.querySelector(
797
+ `.${styles.paginationListItem}:first-child .${styles.paginationListItemBtn}`
798
+ ).innerHTML = caretLeftIcon;
799
+
800
+ pagination.querySelector(
801
+ `.${styles.paginationListItem}:last-child .${styles.paginationListItemBtn}`
802
+ ).innerHTML = caretRightIcon;
803
+
804
+ pagination.querySelectorAll(`.${styles.paginationListItemBtn}:has(svg)`)
805
+ .forEach(btn => btn.classList.add(styles.hasSvg));
806
+ }
807
+ }
779
808
 
780
- // Ripristino pagina dal cookie di sessione (se referrer autorizzato)
809
+ // ── Ripristino pagina dal cookie di sessione (se referrer autorizzato) ─
781
810
  // const isRefUrl = this._isReferrer(), refsCallback = this._getParam('refsCallback', null);
782
811
  // if (isRefUrl) {
783
812
  if (this._isReferrer()) {
@@ -793,6 +822,7 @@ class SimpleDatatableAdapter extends HTMLElement {
793
822
  // refsCallback(isRefUrl);
794
823
  // }
795
824
 
825
+ // ── Filtro contestuale post-reload ────────────────────────────────────
796
826
  // Applica eventuale filtro contestuale passato via reload({ search: … })
797
827
  // Lo facciamo qui, dentro datatable.init, per essere certi che
798
828
  // simple-datatables abbia completato il render dei nuovi dati.
@@ -807,14 +837,11 @@ class SimpleDatatableAdapter extends HTMLElement {
807
837
  }
808
838
  }
809
839
 
810
- // Renderizza il tfoot (se almeno una colonna ha _footerRender)
840
+ // ── Tfoot, info, top slot ─────────────────────────────────────────────
841
+ // Eseguiti sempre, indipendentemente dalla presenza della paginazione.
811
842
  this._renderTfoot();
812
-
813
- // Aggiorna il testo info con il totale assoluto
843
+ this._injectTopSlot(); // va prima di _updateInfo per creare il nodo top-info
814
844
  this._updateInfo();
815
-
816
- // Inietta l'elemento nello slot top-sinistra (se configurato)
817
- this._injectTopSlot();
818
845
  });
819
846
 
820
847
  // Salva la pagina corrente nel cookie ad ogni cambio di pagina;
@@ -927,84 +954,123 @@ class SimpleDatatableAdapter extends HTMLElement {
927
954
  /**
928
955
  * Sovrascrive il testo dell'area `.info` generato da simple-datatables per
929
956
  * includere sempre il totale assoluto dei record (indipendente dal filtro).
957
+ * Se `showInfoAtTop` è attivo, aggiorna in sincronia anche il nodo `.info`
958
+ * nell'area superiore (creato da `_injectTopSlot`).
959
+ *
960
+ * Calcolo di start/end: si conta il numero di righe renderizzate nel tbody
961
+ * (`dt.pages[currentPage - 1]`) invece di fare aritmetica su perPage,
962
+ * perché `dt.currentPage` può essere undefined al primo render.
930
963
  *
931
964
  * Comportamento:
932
965
  * - Senza ricerca attiva:
933
966
  * "Stai visualizzando le righe da {start} a {end}, su un totale di {total}."
934
- * - Con ricerca attiva:
935
- * "Stai visualizzando le righe da {start} a {end} dei {filtered} record filtrati,
936
- * su un totale di {total}."
967
+ * - Con ricerca attiva (risultati > 0):
968
+ * "Stai visualizzando le righe da {start} a {end} dei {filtered} record
969
+ * filtrati, su un totale di {total}."
937
970
  * - Nessun risultato (ricerca attiva, 0 match):
938
971
  * "Nessun risultato per la ricerca corrente (totale record: {total})."
939
972
  *
940
- * Viene chiamato da datatable.init, datatable.page, datatable.search,
941
- * datatable.multisearch.
973
+ * Il nodo top-info viene nascosto quando filtered = 0 o total = 0.
942
974
  */
943
975
  _updateInfo() {
944
- const infoEl = this.querySelector(`.${styles.info}`);
945
- if (!infoEl) return;
976
+ const infoEl = this.querySelector(`.${styles.info}`);
977
+ const topInfoEl = this.querySelector('.datatable-top-info');
946
978
 
947
- const dt = this._dt;
948
- const total = this._rawData?.length ?? 0;
979
+ if (!infoEl && !topInfoEl) return;
949
980
 
950
- // Numero di record nel set corrente (filtrati o tutti)
981
+ const dt = this._dt;
982
+ const total = this._rawData?.length ?? 0;
951
983
  const filtered = dt.searching ? dt.searchData.length : total;
952
984
 
953
- if (filtered === 0 && dt.searching) {
954
- infoEl.textContent = `Nessun risultato per la ricerca corrente (totale record: ${total}).`;
985
+ let text;
986
+
987
+ if (total === 0 || (filtered === 0 && dt.searching)) {
988
+ // Nessun risultato — nasconde il nodo top-info se presente
989
+ if (topInfoEl) topInfoEl.hidden = true;
990
+
991
+ text = filtered === 0 && dt.searching
992
+ ? `Nessun risultato per la ricerca corrente (totale record: ${total}).`
993
+ : `Nessun record trovato.`;
994
+
995
+ if (infoEl) infoEl.textContent = text;
955
996
  return;
956
997
  }
957
998
 
958
- // Calcola start/end della pagina corrente
959
- const perPage = dt.options.perPage;
960
- const currentPage = dt.currentPage; // 1-based
961
- const start = (currentPage - 1) * perPage + 1;
962
- const end = Math.min(currentPage * perPage, filtered);
999
+ // Calcola start/end dalla pagina corrente in dt.pages (più affidabile di
1000
+ // currentPage * perPage, che può dare NaN se currentPage è undefined
1001
+ // al primo render oppure quando non c'è paginazione).
1002
+ const currentPage = dt.currentPage ?? 1;
1003
+ const pageRows = dt.pages?.[currentPage - 1] ?? [];
1004
+ const perPage = dt.options?.perPage ?? filtered;
1005
+ const start = pageRows.length > 0
1006
+ ? (currentPage - 1) * perPage + 1
1007
+ : 1;
1008
+ const end = pageRows.length > 0
1009
+ ? start + pageRows.length - 1
1010
+ : filtered;
963
1011
 
964
1012
  if (dt.searching) {
965
- infoEl.textContent =
1013
+ text =
966
1014
  `Stai visualizzando le righe da ${start} a ${end} ` +
967
1015
  `dei ${filtered} record filtrati, su un totale di ${total}.`;
968
1016
  } else {
969
- infoEl.textContent =
1017
+ text =
970
1018
  `Stai visualizzando le righe da ${start} a ${end}, ` +
971
1019
  `su un totale di ${total}.`;
972
1020
  }
1021
+
1022
+ if (infoEl) infoEl.textContent = text;
1023
+ if (topInfoEl) { topInfoEl.textContent = text; topInfoEl.hidden = false; }
973
1024
  }
974
1025
 
975
1026
 
976
1027
  // ─── Top slot ────────────────────────────────────────────────────────────────
977
1028
 
978
1029
  /**
979
- * Inietta un elemento DOM o del markup HTML nell'area in alto a sinistra
980
- * del datatable — lo spazio normalmente occupato dal selettore "righe per
981
- * pagina" (qui disabilitato con `perPageSelect: false`).
1030
+ * Inietta nell'area in alto a sinistra del datatable il contenuto configurato.
1031
+ *
1032
+ * Modalità:
1033
+ *
1034
+ * showInfoAtTop: true (ha precedenza su topSlot)
1035
+ * Inserisce un nodo `.datatable-top-info` con la stessa stringa dell'area
1036
+ * `.info` in basso, mantenuta in sync da `_updateInfo`. Viene nascosto
1037
+ * automaticamente quando filtered = 0 o total = 0.
1038
+ * Il parametro `topSlot`, se presente, viene ignorato.
982
1039
  *
983
- * Il parametro `topSlot` accetta:
984
- * - un `Element` (già costruito fuori dal componente)
985
- * - una stringa HTML (viene parsata con `innerHTML`)
1040
+ * topSlot: Element | string (usato solo se showInfoAtTop è false/assente)
1041
+ * Inserisce un elemento DOM o markup HTML arbitrario, avvolto in un
1042
+ * `<div class="datatable-top-slot">`.
986
1043
  *
987
- * L'elemento viene inserito come primo figlio dell'area `.topArea`,
988
- * prima della search box, avvolto in un `<div class="datatable-top-slot">`.
989
- * Se `topSlot` non è definito, non viene inserito nulla.
1044
+ * In entrambi i casi l'elemento viene inserito come primo figlio di `.topArea`,
1045
+ * prima della search box.
990
1046
  *
991
- * Esempio – stringa HTML:
992
- * el.init({
993
- * topSlot: '<button class="btn btn-sm btn-primary">Esporta CSV</button>',
994
- * });
1047
+ * Esempi:
1048
+ * el.init({ showInfoAtTop: true });
1049
+ *
1050
+ * el.init({ topSlot: '<button class="btn btn-sm btn-primary">Esporta</button>' });
995
1051
  *
996
- * Esempio – elemento DOM:
997
1052
  * const btn = document.createElement('button');
998
- * btn.textContent = 'Nuova commessa';
1053
+ * btn.textContent = 'Nuovo';
999
1054
  * el.init({ topSlot: btn });
1000
1055
  */
1001
1056
  _injectTopSlot() {
1002
- const topSlot = this._getParam('topSlot', null);
1003
- if (topSlot == null) return;
1004
-
1005
1057
  const topArea = this.querySelector(`.${styles.topArea}`);
1006
1058
  if (!topArea) return;
1007
1059
 
1060
+ const showInfoAtTop = this._getParam('showInfoAtTop', true);
1061
+
1062
+ if (showInfoAtTop) {
1063
+ // Crea il nodo info top (il testo verrà scritto da _updateInfo)
1064
+ const infoTop = document.createElement('div');
1065
+ infoTop.className = `datatable-top-info ${styles.info}`;
1066
+ infoTop.hidden = true; // nascosto finché _updateInfo non lo popola
1067
+ topArea.insertBefore(infoTop, topArea.firstChild);
1068
+ return;
1069
+ }
1070
+
1071
+ const topSlot = this._getParam('topSlot', null);
1072
+ if (topSlot == null) return;
1073
+
1008
1074
  const wrapper = document.createElement('div');
1009
1075
  wrapper.className = 'datatable-top-slot';
1010
1076
 
@@ -1014,7 +1080,6 @@ class SimpleDatatableAdapter extends HTMLElement {
1014
1080
  wrapper.innerHTML = topSlot;
1015
1081
  }
1016
1082
 
1017
- // Inserisce prima della search box (primo figlio esistente)
1018
1083
  topArea.insertBefore(wrapper, topArea.firstChild);
1019
1084
  }
1020
1085
 
@@ -19,6 +19,11 @@
19
19
  align-items: center;
20
20
  justify-content: flex-end;
21
21
  padding-block-end: var(--size-xs);
22
+
23
+ /* stylelint-disable-next-line selector-class-pattern */
24
+ &:has(:global(.datatable-top-info)), &:has(:global(.datatable-top-slot)) {
25
+ justify-content: space-between;
26
+ }
22
27
  }
23
28
 
24
29
  .searchWrapper {
@@ -1,66 +0,0 @@
1
- {
2
- "table": {
3
- "thead": {
4
- "background": {
5
- "color": {
6
- "$type": "color",
7
- "$value": "#ddd"
8
- }
9
- },
10
- "border": {
11
- "width" : {
12
- "$type": "dimension",
13
- "$value": "3px"
14
- }
15
- }
16
- },
17
-
18
- "body": {
19
- "background": {
20
- "color": null
21
- },
22
- "border": {
23
- "width" : {
24
- "$type": "dimension",
25
- "$value": "1px"
26
- }
27
- },
28
- "tr": {
29
- "alt-bg-color": {
30
- "$type": "color",
31
- "$value": "#efefef"
32
- },
33
- "hover-bg-color": {
34
- "$type": "color",
35
- "$value": "#cbcbcb"
36
- }
37
- }
38
- },
39
-
40
- "border": {
41
- "color" : {
42
- "$type": "color",
43
- "$value": "#333"
44
- }
45
- },
46
-
47
- "cell": {
48
- "padding" : {
49
- "block": {
50
- "$type": "dimension",
51
- "$value": ".3rem"
52
- },
53
- "inline": {
54
- "$type": "dimension",
55
- "$value": ".6rem"
56
- }
57
- },
58
- "line-height": {
59
- "$type": "number",
60
- "$value": "1.3"
61
- }
62
- }
63
-
64
-
65
- }
66
- }