@massimo-cassandro/minimo 1.32.4 → 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 (88) hide show
  1. package/README.md +2 -1
  2. package/dev-tools/starter-kit/source_files/_private/pwd.md +16 -0
  3. package/dev-tools/starter-kit/source_files/_private/sf.md +12 -0
  4. package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +2 -1
  5. package/dev-tools/starter-kit/source_files/app/assets/api_htaccess +20 -0
  6. package/dev-tools/starter-kit/source_files/{layout-critical.css → app/css/layout.critical.css} +1 -1
  7. package/dev-tools/starter-kit/source_files/app/css/layout.css +0 -0
  8. package/dev-tools/starter-kit/source_files/app/index.js +1 -0
  9. package/dev-tools/starter-kit/{package-tpl.json → source_files/root/package-tpl.json} +1 -1
  10. package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +1 -1
  11. package/dev-tools/starter-kit/source_files/root/webpack-config-modules/inline-critical-css.mjs +41 -0
  12. package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-setup.mjs +1 -1
  13. package/dev-tools/starter-kit/{webpack.config.mjs → source_files/root/webpack.config.mjs} +56 -38
  14. package/dev-tools/starter-kit/starter-install.sh +233 -81
  15. package/dev-tools/stylelint-config/README.md +45 -0
  16. package/dev-tools/stylelint-config/index.js +184 -0
  17. package/dev-tools/stylelint-config/test/test.module.scss +9 -0
  18. package/package.json +11 -2
  19. package/snippets/php/iviewer.php +488 -0
  20. package/snippets/twig-symfony/ViewerController.php +549 -0
  21. package/src/custom-properties.css +36 -0
  22. package/src/minimo.css +3 -0
  23. package/src/utilities/dom-builder/dom-builder.js +9 -5
  24. package/src/web-components/json-table/json-table-component.js +632 -0
  25. package/src/web-components/json-table/json-table-component.module.css +247 -0
  26. package/src/web-components/json-table/json-table.minimo.tokens.mjs +243 -0
  27. package/src/web-components/json-table/readme.md +764 -0
  28. package/src/web-components/json-table/src/cell-content.js +136 -0
  29. package/src/web-components/json-table/src/content-utils.js +103 -0
  30. package/src/web-components/json-table/src/data-types.js +280 -0
  31. package/src/web-components/json-table/src/defaults.js +424 -0
  32. package/src/web-components/json-table/src/get-data.js +152 -0
  33. package/src/web-components/json-table/src/icons.js +80 -0
  34. package/src/web-components/json-table/src/info-section.js +107 -0
  35. package/src/web-components/json-table/src/main-builder.js +218 -0
  36. package/src/web-components/json-table/src/pagination.js +253 -0
  37. package/src/web-components/json-table/src/parse-cols.js +113 -0
  38. package/src/web-components/json-table/src/parse-rows.js +81 -0
  39. package/src/web-components/json-table/src/resolve-params.js +166 -0
  40. package/src/web-components/json-table/src/search.js +71 -0
  41. package/src/web-components/json-table/src/sorting.js +111 -0
  42. package/src/web-components/json-table/src/table-body.js +109 -0
  43. package/src/web-components/json-table/src/table-builder.js +152 -0
  44. package/src/web-components/json-table/src/table-tfoot.js +56 -0
  45. package/src/web-components/json-table/src/table-thead.js +183 -0
  46. package/src/web-components/json-table/src/update-info.js +67 -0
  47. package/src/web-components/s-datatable-component/{docs.md → readme.md} +67 -3
  48. package/src/web-components/s-datatable-component/s-datatable-component.js +1 -1
  49. package/types/global.d.ts +17 -3
  50. package/dev-tools/starter-kit/source_files/app_index.js +0 -1
  51. package/snippets/config-templates/_htaccess +0 -69
  52. package/snippets/config-templates/robots.txt +0 -5
  53. package/snippets/config-templates/rollup.config.mjs +0 -115
  54. /package/dev-tools/starter-kit/source_files/{_root_robots.txt → app/assets/robots.txt} +0 -0
  55. /package/dev-tools/starter-kit/source_files/{_root_htaccess → app/assets/root_htaccess} +0 -0
  56. /package/{src → dev-tools/starter-kit/source_files/app/css}/fonts.css +0 -0
  57. /package/dev-tools/starter-kit/source_files/{favicons-readme.md → app/favicons/favicons-readme.md} +0 -0
  58. /package/dev-tools/starter-kit/{webpack-template.ejs → source_files/app/tpl/index.ejs} +0 -0
  59. /package/dev-tools/starter-kit/source_files/{__project__.code-workspace → root/__project__.code-workspace} +0 -0
  60. /package/dev-tools/starter-kit/source_files/{_browserslistrc → root/_browserslistrc} +0 -0
  61. /package/dev-tools/starter-kit/source_files/{_editorconfig → root/_editorconfig} +0 -0
  62. /package/dev-tools/starter-kit/source_files/{_gitignore → root/_gitignore} +0 -0
  63. /package/dev-tools/starter-kit/source_files/{_prettierrc → root/_prettierrc} +0 -0
  64. /package/dev-tools/starter-kit/source_files/{eslint.config.mjs → root/eslint.config.mjs} +0 -0
  65. /package/dev-tools/starter-kit/source_files/{jsconfig.json → root/jsconfig.json} +0 -0
  66. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/css-rules.mjs +0 -0
  67. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/get-jsConfig-aliases.mjs +0 -0
  68. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/mini-svg-data-uri-loader.cjs +0 -0
  69. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/postcss.config.mjs +0 -0
  70. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-variables-safelist.mjs +0 -0
  71. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svg-rules.mjs +0 -0
  72. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svgo.config.mjs +0 -0
  73. /package/snippets/{config-templates/_github → _github}/dependabot.yml +0 -0
  74. /package/snippets/{error-pages/error-pages.js → error-pages.js} +0 -0
  75. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.jsx +0 -0
  76. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.module.scss +0 -0
  77. /package/snippets/{config-templates → root}/_hintrc.cjs +0 -0
  78. /package/snippets/{config-templates → root}/_npmignore +0 -0
  79. /package/snippets/{config-templates → root}/_npmrc +0 -0
  80. /package/snippets/{config-templates → root}/_stylelintignore +0 -0
  81. /package/snippets/{config-templates → root}/babel.config--react.json +0 -0
  82. /package/snippets/{config-templates → root}/babel.config--solidJS.json +0 -0
  83. /package/snippets/{ImgViewerController.php → twig-symfony/ImgViewerController.php} +0 -0
  84. /package/snippets/{UnsplashController.php → twig-symfony/UnsplashController.php} +0 -0
  85. /package/snippets/{config-templates → twig-symfony}/_env.local +0 -0
  86. /package/snippets/{error-pages → twig-symfony}/bundles/TwigBundle/Exception/error.html.twig +0 -0
  87. /package/snippets/{flash-messages.html.twig → twig-symfony/flash-messages.html.twig} +0 -0
  88. /package/snippets/{minimo-form-layout.html.twig → twig-symfony/minimo-form-layout.html.twig} +0 -0
@@ -0,0 +1,107 @@
1
+ /*! minimo - json-table: info section parts */
2
+
3
+ import * as styles from '../json-table-component.module.css';
4
+ import { classnames } from '../../../utilities/classnames.js';
5
+
6
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
7
+ /** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
8
+
9
+ /**
10
+ * domBuilder item of the info text container (template slot `resultInfo`).
11
+ *
12
+ * Generated structure: `div.resultInfo[classes.resultInfo][aria-live=polite]`; the content is
13
+ * set by `update-info.js`. The element is stored in `elements.resultInfo`.
14
+ *
15
+ * @param {JsonTableParams} params - Resolved params
16
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
17
+ * @returns {DomBuilderItem}
18
+ *
19
+ * @example
20
+ * domBuilder([resultInfoPart(params, elements)], host);
21
+ * elements.resultInfo; // → HTMLElement
22
+ */
23
+ export function resultInfoPart(params, elements) {
24
+ return {
25
+ className: classnames(styles.resultInfo, params.classes.resultInfo),
26
+ attrs: { 'aria-live': 'polite' },
27
+ callback: el => { elements.resultInfo = /** @type {HTMLElement} */ (el); }
28
+ };
29
+ }
30
+
31
+
32
+ /**
33
+ * domBuilder item of the search input (template slot `search`), rendered only when
34
+ * `params.search` is true.
35
+ *
36
+ * Generated structure: `div.search[classes.search] > input[type=search][classes.searchInput]`.
37
+ * The input is stored in `elements.searchInput`.
38
+ *
39
+ * TODO listener sull'input di ricerca: step 3
40
+ *
41
+ * @param {JsonTableParams} params - Resolved params
42
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
43
+ * @returns {DomBuilderItem}
44
+ *
45
+ * @example
46
+ * domBuilder([searchPart(params, elements)], host);
47
+ * elements.searchInput; // → HTMLInputElement (when params.search is true)
48
+ */
49
+ export function searchPart(params, elements) {
50
+ return {
51
+ condition: params.search,
52
+ className: classnames(styles.search, params.classes.search),
53
+ children: [
54
+ {
55
+ tag: 'input',
56
+ className: classnames(params.classes.searchInput),
57
+ attrs: {
58
+ type: 'search',
59
+ title: params.labels.searchTitle,
60
+ placeholder: params.labels.searchPlaceholder,
61
+ 'aria-label': params.labels.searchAriaLabel
62
+ },
63
+ callback: el => { elements.searchInput = /** @type {HTMLInputElement} */ (el); }
64
+ }
65
+ ]
66
+ };
67
+ }
68
+
69
+
70
+ /**
71
+ * domBuilder item of the whole info section (template slot `infoSection`): info text + search input.
72
+ *
73
+ * Generated structure:
74
+ * ```
75
+ * div.infoOuter[classes.infoOuter]
76
+ * div.info[classes.info]
77
+ * div.resultInfo ← resultInfoPart()
78
+ * div.search > input[type=search] ← searchPart(), only if `params.search` is true
79
+ * ```
80
+ *
81
+ * References to the generated elements are stored in `elements`
82
+ * (`infoOuter`, `info`, `resultInfo`, `searchInput`).
83
+ *
84
+ * @param {JsonTableParams} params - Resolved params
85
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
86
+ * @returns {DomBuilderItem}
87
+ *
88
+ * @example
89
+ * domBuilder([{ tag: 'section', children: [infoSectionPart(params, elements)] }], host);
90
+ * elements.info; // → HTMLElement
91
+ */
92
+ export function infoSectionPart(params, elements) {
93
+ return {
94
+ className: classnames(styles.infoOuter, params.classes.infoOuter),
95
+ callback: el => { elements.infoOuter = /** @type {HTMLElement} */ (el); },
96
+ children: [
97
+ {
98
+ className: classnames(styles.info, params.classes.info),
99
+ callback: el => { elements.info = /** @type {HTMLElement} */ (el); },
100
+ children: [
101
+ resultInfoPart(params, elements),
102
+ searchPart(params, elements)
103
+ ]
104
+ }
105
+ ]
106
+ };
107
+ }
@@ -0,0 +1,218 @@
1
+ /*! minimo - json-table: main structure builder */
2
+
3
+ import * as styles from '../json-table-component.module.css';
4
+ import { domBuilder } from '../../../utilities/dom-builder/dom-builder.js';
5
+ import { classnames } from '../../../utilities/classnames.js';
6
+ import { defaults } from './defaults.js';
7
+ import { infoSectionPart, resultInfoPart, searchPart } from './info-section.js';
8
+ import { tablePart, tableFooterPart, captionPart, captionId } from './table-builder.js';
9
+ import { paginationPart } from './pagination.js';
10
+
11
+ /** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
12
+ /** @typedef {import('./defaults.js').TemplateItem} TemplateItem */
13
+ /** @typedef {import('./defaults.js').TemplateSlot} TemplateSlot */
14
+
15
+ /**
16
+ * Elements generated by the component, collected during the build.
17
+ * @typedef {Object} JsonTableElements
18
+ * @property {HTMLElement} [wrapper] - Main wrapper (`section`), contains the template parts
19
+ * @property {HTMLElement} [infoOuter] - Outer info container
20
+ * @property {HTMLElement} [info] - Info container (flex row: info text + search)
21
+ * @property {HTMLElement} [resultInfo] - Info text container
22
+ * @property {HTMLInputElement} [searchInput] - Search input (only when `search` is true)
23
+ * @property {HTMLElement} [tableWrapper] - Div wrapping the table (`table-responsive` by default)
24
+ * @property {HTMLTableElement} [table] - The `<table>` element
25
+ * @property {HTMLTableSectionElement} [thead] - The `<thead>` element
26
+ * @property {HTMLTableSectionElement} [tbody] - The `<tbody>` element
27
+ * @property {HTMLTableSectionElement} [tfoot] - The `<tfoot>` element (only when `tfoot` is true)
28
+ * @property {HTMLElement} [tableFooter] - Footer bar below the table (caption + pagination), when rendered
29
+ * @property {HTMLElement} [caption] - Caption container (only when `caption` is set)
30
+ * @property {HTMLElement} [pagination] - Pagination `<nav>` (only when `perPage` > 0)
31
+ */
32
+
33
+ /**
34
+ * Whether a template item is a slot placeholder.
35
+ * @param {*} item
36
+ * @returns {item is { slot: TemplateSlot }}
37
+ */
38
+ function isSlot(item) {
39
+ return item != null && typeof item === 'object' && !(item instanceof Node) && typeof item.slot === 'string';
40
+ }
41
+
42
+
43
+ /**
44
+ * Collects, recursively (`children` and array `content`), the names of the slots used by a template.
45
+ *
46
+ * @param {Array<TemplateItem|string|Node>} items - Template items
47
+ * @param {Set<string>} [found=new Set()] - Accumulator (default: new Set())
48
+ * @returns {Set<string>}
49
+ *
50
+ * @example
51
+ * collectSlots([{ slot: 'table' }, { children: [{ slot: 'pagination' }] }]); // → Set { 'table', 'pagination' }
52
+ */
53
+ export function collectSlots(items, found = new Set()) {
54
+
55
+ items.forEach(item => {
56
+ if (isSlot(item)) {
57
+ found.add(item.slot);
58
+ } else if (item != null && typeof item === 'object' && !(item instanceof Node)) {
59
+ if (Array.isArray(item.children)) {
60
+ collectSlots(item.children, found);
61
+ }
62
+ if (Array.isArray(item.content)) {
63
+ collectSlots(/** @type {TemplateItem[]} */ (item.content), found);
64
+ }
65
+ }
66
+ });
67
+
68
+ return found;
69
+ }
70
+
71
+
72
+ /**
73
+ * Replaces, recursively (`children` and array `content`), every `{ slot: 'name' }` item of a
74
+ * template with the corresponding built-in part. Unknown slots are reported in the console
75
+ * and skipped. Non-slot items are shallow-copied, so the original template is never mutated.
76
+ *
77
+ * @param {Array<TemplateItem|string|Node>} items - Template items
78
+ * @param {Object<string, DomBuilderItem>} parts - Built-in parts, keyed by slot name
79
+ * @returns {Array<DomBuilderItem|string|Node>}
80
+ *
81
+ * @example
82
+ * expandSlots([{ className: 'row', children: [{ slot: 'search' }, { slot: 'resultInfo' }] }, { slot: 'table' }], parts);
83
+ * // → [{ className: 'row', children: [<search part>, <resultInfo part>] }, <table part>]
84
+ */
85
+ export function expandSlots(items, parts) {
86
+
87
+ /** @type {Array<DomBuilderItem|string|Node>} */
88
+ const result = [];
89
+
90
+ items.forEach(item => {
91
+
92
+ if (item == null || typeof item !== 'object' || item instanceof Node) {
93
+ result.push(/** @type {string|Node} */ (item));
94
+ return;
95
+ }
96
+
97
+ if (isSlot(item)) {
98
+ const part = parts[item.slot];
99
+ if (!part) {
100
+ // eslint-disable-next-line no-console
101
+ console.error(`[json-table] template: slot \`${item.slot}\` non riconosciuto (disponibili: ${Object.keys(parts).join(', ')})`);
102
+ return;
103
+ }
104
+ result.push(part);
105
+ return;
106
+ }
107
+
108
+ /** @type {DomBuilderItem} */
109
+ const copy = /** @type {DomBuilderItem} */ ({ ...item });
110
+
111
+ if (Array.isArray(item.children)) {
112
+ copy.children = expandSlots(item.children, parts);
113
+ }
114
+ if (Array.isArray(item.content)) {
115
+ copy.content = /** @type {DomBuilderItem[]} */ (expandSlots(/** @type {TemplateItem[]} */ (item.content), parts));
116
+ }
117
+
118
+ result.push(copy);
119
+ });
120
+
121
+ return result;
122
+ }
123
+
124
+
125
+ /**
126
+ * Builds the whole component structure inside the host element (emptied first) via domBuilder:
127
+ * a fixed main wrapper whose content is defined by `params.template`.
128
+ *
129
+ * Available template slots (see `TemplateItem`):
130
+ * - `infoSection` → `infoSectionPart()` (info text + search input)
131
+ * - `resultInfo` → `resultInfoPart()` (info text only)
132
+ * - `search` → `searchPart()` (search input only)
133
+ * - `table` → `tablePart()` (table wrapper + table with thead, tbody, tfoot, plus the footer bar)
134
+ * - `caption` → `captionPart()` (caption container; part of the `table` footer bar unless placed explicitly)
135
+ * - `pagination` → `paginationPart()` (pagination nav; idem)
136
+ *
137
+ * Generated structure with the default template (`[{ slot: 'infoSection' }, { slot: 'table' }]`):
138
+ * ```
139
+ * section.wrapper[classes.wrapper]
140
+ * div.infoOuter[classes.infoOuter]
141
+ * div.info[classes.info]
142
+ * div.resultInfo
143
+ * div.search > input[type=search] (if `search`)
144
+ * div.tableOuter
145
+ * div.tableWrapper[classes.tableWrapper]
146
+ * table#tableId.table[classes.table]
147
+ * thead / tbody / tfoot (tfoot if `tfoot`)
148
+ * div.tableFooter[classes.tableFooter] (if `caption` or `perPage` > 0)
149
+ * div.caption (if `caption`)
150
+ * nav.pagination (if `perPage` > 0)
151
+ * ```
152
+ *
153
+ * The `<tbody>`, `<tfoot>` and pagination are created empty: see `renderTbody()`, `renderTfoot()`
154
+ * and `renderPagination()`.
155
+ *
156
+ * @param {JsonTable} jt - The component instance (host element, `params`, `cols`)
157
+ * @returns {JsonTableElements} References to the generated elements
158
+ *
159
+ * @example
160
+ * jt.params.template = [
161
+ * { className: 'flex gap-2', children: [{ slot: 'search' }, { slot: 'pagination' }] },
162
+ * { slot: 'table' }, // footer bar with the caption only: the pagination is placed above
163
+ * { slot: 'resultInfo' }
164
+ * ];
165
+ * const elements = mainBuilder(jt);
166
+ * elements.table; // → HTMLTableElement
167
+ */
168
+ export function mainBuilder(jt) {
169
+
170
+ const { params } = jt;
171
+
172
+ /** @type {JsonTableElements} */
173
+ const elements = {};
174
+
175
+ const capId = captionId(params);
176
+ const captionItem = captionPart(jt, elements, capId);
177
+ const paginationItem = paginationPart(jt, elements);
178
+
179
+ // footer bar parts: removed below when the template places them elsewhere
180
+ /** @type {DomBuilderItem[]} */
181
+ const footerChildren = [captionItem, paginationItem];
182
+ const footerBar = tableFooterPart(jt, elements, footerChildren);
183
+
184
+ /** @type {Object<string, DomBuilderItem>} */
185
+ const parts = {
186
+ infoSection: infoSectionPart(params, elements),
187
+ resultInfo: resultInfoPart(params, elements),
188
+ search: searchPart(params, elements),
189
+ caption: captionItem,
190
+ pagination: paginationItem,
191
+ table: tablePart(jt, elements, footerBar, capId)
192
+ };
193
+
194
+ const template = typeof params.template === 'function'
195
+ ? params.template(parts, params)
196
+ : params.template;
197
+
198
+ const items = Array.isArray(template) ? template : /** @type {TemplateItem[]} */ (defaults.template);
199
+ const usedSlots = collectSlots(items);
200
+
201
+ ['caption', 'pagination'].forEach(slot => {
202
+ if (usedSlots.has(slot)) {
203
+ footerChildren.splice(footerChildren.indexOf(parts[slot]), 1);
204
+ }
205
+ });
206
+ footerBar.condition = footerChildren.some(item => item.condition !== false);
207
+
208
+ domBuilder([
209
+ {
210
+ tag: 'section',
211
+ className: classnames(styles.wrapper, params.classes.wrapper),
212
+ callback: el => { elements.wrapper = /** @type {HTMLElement} */ (el); },
213
+ children: expandSlots(items, parts)
214
+ }
215
+ ], jt, { emptyParent: true });
216
+
217
+ return elements;
218
+ }
@@ -0,0 +1,253 @@
1
+ /*! minimo - json-table: pagination */
2
+
3
+ import * as styles from '../json-table-component.module.css';
4
+ import { domBuilder } from '../../../utilities/dom-builder/dom-builder.js';
5
+ import { classnames } from '../../../utilities/classnames.js';
6
+ import { iconContent } from './icons.js';
7
+
8
+ /** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
9
+ /** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
10
+
11
+ /**
12
+ * Number of pages needed to show `count` rows, `perPage` per page (at least 1).
13
+ * `perPage` <= 0 means no pagination: a single page.
14
+ *
15
+ * @param {number} count - Number of rows
16
+ * @param {number} perPage - Rows per page
17
+ * @returns {number}
18
+ *
19
+ * @example
20
+ * calcTotPages(101, 25); // → 5
21
+ * calcTotPages(0, 25); // → 1
22
+ * calcTotPages(101, 0); // → 1
23
+ */
24
+ export function calcTotPages(count, perPage) {
25
+ if (!(perPage > 0)) {
26
+ return 1;
27
+ }
28
+ return Math.max(1, Math.ceil(count / perPage));
29
+ }
30
+
31
+
32
+ /**
33
+ * Builds the list of page numbers to show in the navigation: the current page with `delta`
34
+ * pages on each side (the window is widened near the edges so that it keeps a constant size),
35
+ * plus the first and the last page; `null` marks a gap (ellipsis). When the gap would hide a
36
+ * single page, that page is shown instead of the ellipsis.
37
+ *
38
+ * @param {number} current - Current page (1-based)
39
+ * @param {number} totPages - Total number of pages
40
+ * @param {number} [delta=2] - Pages on each side of the current one (default: 2)
41
+ * @returns {Array<number|null>}
42
+ *
43
+ * @example
44
+ * pageList(1, 3); // → [1, 2, 3]
45
+ * pageList(1, 10); // → [1, 2, 3, 4, 5, null, 10]
46
+ * pageList(6, 12); // → [1, null, 4, 5, 6, 7, 8, null, 12]
47
+ * pageList(5, 10); // → [1, 2, 3, 4, 5, 6, 7, null, 10] (page 2 shown instead of an ellipsis hiding it alone)
48
+ * pageList(10, 10); // → [1, null, 6, 7, 8, 9, 10]
49
+ * pageList(4, 10, 1); // → [1, 2, 3, 4, 5, null, 10]
50
+ */
51
+ export function pageList(current, totPages, delta = 2) {
52
+
53
+ const side = Math.max(0, Math.floor(delta));
54
+ const width = side * 2 + 1;
55
+
56
+ if (totPages <= width + 2) {
57
+ return Array.from({ length: totPages }, (_, i) => i + 1);
58
+ }
59
+
60
+ let min = Math.max(1, current - side);
61
+ let max = Math.min(totPages, current + side);
62
+
63
+ // keep the window size constant near the edges
64
+ while (max - min + 1 < width) {
65
+ if (min > 1) {
66
+ min--;
67
+ } else if (max < totPages) {
68
+ max++;
69
+ } else {
70
+ break;
71
+ }
72
+ }
73
+
74
+ /** @type {Array<number|null>} */
75
+ const pages = Array.from({ length: max - min + 1 }, (_, i) => min + i);
76
+
77
+ if (min === 3) {
78
+ pages.unshift(1, 2);
79
+ } else if (min > 3) {
80
+ pages.unshift(1, null);
81
+ } else if (min === 2) {
82
+ pages.unshift(1);
83
+ }
84
+
85
+ if (max === totPages - 2) {
86
+ pages.push(totPages - 1, totPages);
87
+ } else if (max < totPages - 2) {
88
+ pages.push(null, totPages);
89
+ } else if (max === totPages - 1) {
90
+ pages.push(totPages);
91
+ }
92
+
93
+ return pages;
94
+ }
95
+
96
+
97
+ /**
98
+ * Replaces the `{page}` placeholder of a pagination label.
99
+ * @param {string|undefined} label
100
+ * @param {number} page
101
+ * @param {string} locale
102
+ * @returns {string}
103
+ */
104
+ function pageLabel(label, page, locale) {
105
+ return String(label ?? '').replace(/\{page\}/g, page.toLocaleString(locale));
106
+ }
107
+
108
+
109
+ /**
110
+ * domBuilder item of the pagination `<nav>` (template slot `pagination`), rendered only when
111
+ * `params.perPage` is > 0. The `<nav>` is stored in `elements.pagination` and gets a delegated
112
+ * `click` listener on its buttons, which calls `jt.goToPage()`. The content (page list) is
113
+ * rendered by `renderPagination()` and the whole `<nav>` is hidden when there is a single page.
114
+ *
115
+ * Generated structure (see `renderPagination`):
116
+ * ```
117
+ * nav.pagination[classes.pagination][aria-label=labels.paginationAriaLabel]
118
+ * ul.paginationList
119
+ * li.paginationItem > button.paginationBtn[classes.paginationBtn][data-page=prev][aria-label][title] > icon
120
+ * li.paginationItem > button.paginationBtn[data-page=1][aria-label="Vai a pagina 1"] 1
121
+ * li.paginationItem > button.paginationBtn[data-page=2][aria-current=page] 2 ← current page
122
+ * li.paginationItem > span.paginationEllipsis …
123
+ * li.paginationItem > button.paginationBtn[data-page=next] > icon
124
+ * ```
125
+ *
126
+ * @param {JsonTable} jt - The component instance
127
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
128
+ * @returns {DomBuilderItem}
129
+ *
130
+ * @example
131
+ * domBuilder([paginationPart(jt, elements)], host);
132
+ * elements.pagination; // → HTMLElement (nav), when params.perPage > 0
133
+ */
134
+ export function paginationPart(jt, elements) {
135
+
136
+ const { params } = jt;
137
+
138
+ return {
139
+ tag: 'nav',
140
+ condition: params.perPage > 0,
141
+ className: classnames(styles.pagination, params.classes.pagination),
142
+ attrs: { 'aria-label': params.labels.paginationAriaLabel, hidden: '' },
143
+ callback: el => {
144
+ const nav = /** @type {HTMLElement} */ (el);
145
+ elements.pagination = nav;
146
+
147
+ nav.addEventListener('click', e => {
148
+ const btn = /** @type {HTMLElement|null} */ (
149
+ /** @type {HTMLElement} */ (e.target).closest('button[data-page]')
150
+ );
151
+ if (!btn || btn.hasAttribute('disabled') || btn.getAttribute('aria-current') === 'page') {
152
+ return;
153
+ }
154
+ const target = btn.dataset.page;
155
+ const { page } = jt.state;
156
+ const requested = target === 'prev' ? page - 1 : (target === 'next' ? page + 1 : Number(target));
157
+ jt.goToPage(requested, target);
158
+ });
159
+ }
160
+ };
161
+ }
162
+
163
+
164
+ /**
165
+ * Renders the page list inside `elements.pagination` from the current state (`page`, `totPages`),
166
+ * replacing the previous content; the `<nav>` is hidden when there is a single page.
167
+ *
168
+ * When `focusTarget` is given (the `data-page` of the button that triggered the change:
169
+ * `'prev'`, `'next'` or a page number) the focus is moved to the corresponding new button, or
170
+ * to the current page button when that one is no longer available, so that keyboard users do
171
+ * not lose their position.
172
+ *
173
+ * @param {JsonTable} jt - The component instance
174
+ * @param {string|null} [focusTarget=null] - `data-page` of the button to focus after the rendering (default: null)
175
+ * @returns {void}
176
+ *
177
+ * @example
178
+ * renderPagination(jt); // after `jt.state` has been updated
179
+ * renderPagination(jt, 'next'); // same, moving the focus to the "next" button
180
+ */
181
+ export function renderPagination(jt, focusTarget = null) {
182
+
183
+ const { params, elements, state } = jt;
184
+ const nav = elements.pagination;
185
+
186
+ if (!nav) {
187
+ return;
188
+ }
189
+
190
+ if (state.totPages <= 1) {
191
+ nav.hidden = true;
192
+ nav.replaceChildren();
193
+ return;
194
+ }
195
+
196
+ nav.hidden = false;
197
+
198
+ const btnClass = classnames(styles.paginationBtn, params.classes.paginationBtn);
199
+ const { labels, locale } = params;
200
+
201
+ /** @type {(target: string, label: string, icon: import('./defaults.js').IconDef, disabled: boolean) => DomBuilderItem} */
202
+ const arrowBtn = (target, label, icon, disabled) => ({
203
+ tag: 'li',
204
+ className: styles.paginationItem,
205
+ children: [
206
+ {
207
+ tag: 'button',
208
+ className: classnames(btnClass, styles.paginationArrow),
209
+ attrs: { type: 'button', 'data-page': target, 'aria-label': label, title: label, disabled: disabled ? '' : null },
210
+ content: iconContent(icon)
211
+ }
212
+ ]
213
+ });
214
+
215
+ domBuilder([
216
+ {
217
+ tag: 'ul',
218
+ className: styles.paginationList,
219
+ children: [
220
+ arrowBtn('prev', labels.prevPage ?? '', params.paginationPrevIcon, state.page <= 1),
221
+
222
+ ...pageList(state.page, state.totPages, params.paginationDelta).map(page => ({
223
+ tag: 'li',
224
+ className: styles.paginationItem,
225
+ children: [
226
+ page === null
227
+ ? { tag: 'span', className: styles.paginationEllipsis, attrs: { 'aria-hidden': 'true' }, content: '…' }
228
+ : {
229
+ tag: 'button',
230
+ className: btnClass,
231
+ attrs: {
232
+ type: 'button',
233
+ 'data-page': page,
234
+ 'aria-current': page === state.page ? 'page' : null,
235
+ 'aria-label': pageLabel(page === state.page ? labels.currentPage : labels.pageTitle, page, locale),
236
+ title: pageLabel(page === state.page ? labels.currentPage : labels.pageTitle, page, locale)
237
+ },
238
+ content: page.toLocaleString(locale)
239
+ }
240
+ ]
241
+ })),
242
+
243
+ arrowBtn('next', labels.nextPage ?? '', params.paginationNextIcon, state.page >= state.totPages)
244
+ ]
245
+ }
246
+ ], nav, { emptyParent: true });
247
+
248
+ if (focusTarget != null) {
249
+ const wanted = /** @type {HTMLButtonElement|null} */ (nav.querySelector(`button[data-page="${focusTarget}"]:not([disabled])`));
250
+ const current = /** @type {HTMLButtonElement|null} */ (nav.querySelector('button[aria-current="page"]'));
251
+ (wanted ?? current)?.focus();
252
+ }
253
+ }
@@ -0,0 +1,113 @@
1
+ /*! minimo - json-table: columns parsing */
2
+
3
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
4
+ /** @typedef {import('./defaults.js').ColDefinition} ColDefinition */
5
+ /** @typedef {import('./defaults.js').DataTypeDefinition} DataTypeDefinition */
6
+
7
+ /**
8
+ * Column definition after parsing: every property has a value, `condition` is resolved to a
9
+ * boolean, `headerClass`/`cellClass` are the final class strings and `_type` is the data type object.
10
+ * @typedef {Required<Omit<ColDefinition, 'type'|'condition'|'sortValue'|'searchValue'>> & {
11
+ * condition: boolean,
12
+ * sortValue: *,
13
+ * searchValue: *,
14
+ * _type: DataTypeDefinition
15
+ * }} ParsedCol
16
+ */
17
+
18
+ /**
19
+ * Column defaults.
20
+ * @type {Omit<Required<ColDefinition>, 'type'|'sortValue'|'searchValue'> & { sortValue: *, searchValue: * }}
21
+ */
22
+ export const colDefaults = {
23
+ key: '',
24
+ title: null,
25
+ dataType: 'string',
26
+ render: null,
27
+ tfootRender: null,
28
+ rowHeading: false,
29
+ searchable: true,
30
+ sortable: true,
31
+ sortValue: undefined,
32
+ searchValue: undefined,
33
+ condition: true,
34
+ headerClass: null,
35
+ cellClass: null
36
+ };
37
+
38
+
39
+ /**
40
+ * Normalizes the `cols` param: applies the column defaults and the data type `colDefaults`,
41
+ * resolves the `type`/`dataType` alias, the `condition` and the `headerClass`/`cellClass`
42
+ * fallbacks, and drops the columns whose `condition` is false.
43
+ *
44
+ * `cols` is required: an empty or missing definition throws.
45
+ *
46
+ * Merge order (lowest first): `colDefaults` ← data type `colDefaults` ← column definition.
47
+ * Class resolution: `headerClass` = column `headerClass` ?? column `cellClass` ?? type `headerClass`;
48
+ * `cellClass` = column `cellClass` ?? column `headerClass` ?? type `cellClass`.
49
+ *
50
+ * @param {ColDefinition[]|null|undefined} cols - Columns definition (`params.cols`)
51
+ * @param {Object<string, DataTypeDefinition>} dataTypes - Data types map (see `buildDataTypes`)
52
+ * @param {JsonTableParams} params - Resolved params (passed to `condition` functions)
53
+ * @returns {ParsedCol[]} Visible columns only
54
+ * @throws {Error} On missing/empty `cols`, missing `key` or unknown data type
55
+ *
56
+ * @example
57
+ * parseCols([
58
+ * { key: 'id', dataType: 'id' }, // title: 'id' (default: key)
59
+ * { key: 'name', title: 'Nome', cellClass: 'fw-bold' }, // headerClass: 'fw-bold' (default: cellClass)
60
+ * { key: 'amount', type: 'euro', tfootRender: '@sum' }, // `type` alias of `dataType`
61
+ * { key: 'notes', condition: false } // dropped
62
+ * ], dataTypes, params);
63
+ */
64
+ export function parseCols(cols, dataTypes, params) {
65
+
66
+ if (!Array.isArray(cols) || !cols.length) {
67
+ throw new Error('[json-table] parametro `cols` mancante o vuoto: la definizione delle colonne è obbligatoria');
68
+ }
69
+
70
+ return cols
71
+ .map((col, idx) => {
72
+
73
+ if (col == null || typeof col !== 'object') {
74
+ throw new Error(`[json-table] colonna #${idx}: definizione non valida`);
75
+ }
76
+
77
+ const dataType = col.dataType ?? col.type ?? 'string';
78
+ const type = dataTypes[dataType];
79
+
80
+ if (!type) {
81
+ throw new Error(`[json-table] colonna #${idx} (${col.key}): dataType \`${dataType}\` non definito`);
82
+ }
83
+
84
+ // `type` is only an alias of `dataType`: dropped from the merged definition
85
+ const colProps = { ...col };
86
+ delete colProps.type;
87
+
88
+ const merged = {
89
+ ...colDefaults,
90
+ ...(type.colDefaults ?? {}),
91
+ ...colProps,
92
+ dataType
93
+ };
94
+
95
+ if (!merged.key || typeof merged.key !== 'string') {
96
+ throw new Error(`[json-table] colonna #${idx}: parametro \`key\` mancante`);
97
+ }
98
+
99
+ const condition = typeof merged.condition === 'function'
100
+ ? merged.condition(params)
101
+ : (merged.condition ?? true);
102
+
103
+ return /** @type {ParsedCol} */ ({
104
+ ...merged,
105
+ title: merged.title ?? merged.key,
106
+ condition: Boolean(condition),
107
+ headerClass: merged.headerClass ?? merged.cellClass ?? type.headerClass ?? null,
108
+ cellClass: merged.cellClass ?? merged.headerClass ?? type.cellClass ?? null,
109
+ _type: type
110
+ });
111
+ })
112
+ .filter(col => col.condition);
113
+ }