@massimo-cassandro/minimo 1.33.0 → 1.33.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.
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 +163 -82
  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
@@ -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
+ }
@@ -0,0 +1,81 @@
1
+ /*! minimo - json-table: rows parsing (sort & search values) */
2
+
3
+ import { getNestedValue } from './content-utils.js';
4
+
5
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
6
+ /** @typedef {import('./parse-cols.js').ParsedCol} ParsedCol */
7
+
8
+ /**
9
+ * A data row with the values precomputed for sorting and searching.
10
+ * @typedef {Object} ParsedRow
11
+ * @property {number} idx - Index of the row in the original data set
12
+ * @property {Object} row - The original row object
13
+ * @property {Object<string, *>} sortValues - Sort value of every sortable column, keyed by column key
14
+ * @property {string} searchText - Lowercased concatenation of the search values of the searchable columns
15
+ */
16
+
17
+ /**
18
+ * Resolves the sort/search value of a cell: column-level value or function `row => value`
19
+ * (when defined) → data type function `(value, row, params) => value` → raw value.
20
+ * @param {*} colValue - Column `sortValue`/`searchValue` (undefined = not set)
21
+ * @param {((value: *, row: Object, params: JsonTableParams) => *)|undefined} typeFn - Data type function
22
+ * @param {*} value - Raw cell value
23
+ * @param {Object} row - Row object
24
+ * @param {JsonTableParams} params - Resolved params
25
+ * @returns {*}
26
+ */
27
+ function resolveValue(colValue, typeFn, value, row, params) {
28
+ if (colValue !== undefined) {
29
+ return typeof colValue === 'function' ? colValue(row) : colValue;
30
+ }
31
+ if (typeof typeFn === 'function') {
32
+ return typeFn(value, row, params);
33
+ }
34
+ return value;
35
+ }
36
+
37
+
38
+ /**
39
+ * Precomputes, for every row, the values used by sorting and searching, so that they are
40
+ * calculated once and not on every sort/search operation (see `sorting.js` and `search.js`).
41
+ *
42
+ * @param {Array<Object>} rows - Raw data rows
43
+ * @param {ParsedCol[]} cols - Parsed columns (see `parse-cols.js`)
44
+ * @param {JsonTableParams} params - Resolved params
45
+ * @returns {ParsedRow[]}
46
+ *
47
+ * @example
48
+ * parseRows([{ id: 1, name: 'Mario', amount: '12.5' }], cols, params);
49
+ * // → [{ idx: 0, row: {…}, sortValues: { id: 1, name: 'Mario', amount: 12.5 }, searchText: 'mario 12,5' }]
50
+ */
51
+ export function parseRows(rows, cols, params) {
52
+
53
+ return rows.map((row, idx) => {
54
+
55
+ /** @type {Object<string, *>} */
56
+ const sortValues = {};
57
+ /** @type {string[]} */
58
+ const searchParts = [];
59
+
60
+ cols.forEach(col => {
61
+ const value = getNestedValue(row, col.key);
62
+
63
+ if (col.sortable) {
64
+ sortValues[col.key] = resolveValue(col.sortValue, col._type.sortValue, value, row, params);
65
+ }
66
+
67
+ if (col.searchable) {
68
+ const searchValue = resolveValue(
69
+ col.searchValue,
70
+ col._type.searchValue ?? (v => (v == null ? '' : String(v))),
71
+ value, row, params
72
+ );
73
+ if (searchValue != null && searchValue !== '') {
74
+ searchParts.push(String(searchValue));
75
+ }
76
+ }
77
+ });
78
+
79
+ return { idx, row, sortValues, searchText: searchParts.join(' ').toLowerCase() };
80
+ });
81
+ }
@@ -1,22 +1,93 @@
1
1
  /*! minimo - json-table: params resolution */
2
2
 
3
- import { defaults } from './defaults.js';
3
+ import { defaults, mergedParams } from './defaults.js';
4
4
 
5
5
  /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
6
6
 
7
+ /**
8
+ * Whether `value` is a plain object (not null, not an array).
9
+ * @param {*} value
10
+ * @returns {value is Object<string, *>}
11
+ */
12
+ function isPlainObject(value) {
13
+ return value != null && typeof value === 'object' && !Array.isArray(value);
14
+ }
15
+
16
+
17
+ /**
18
+ * Reads and parses an HTML attribute of the `<json-table>` element.
19
+ *
20
+ * - booleans are auto-parsed when the built-in default is a boolean
21
+ * (`search`, `search="true"`, `search="1"` → true; `search="false"`, `search="0"` → false)
22
+ * - numbers are auto-parsed when the built-in default is a number (`perpage="10"` → 10; a
23
+ * non-numeric value is reported in the console and treated as a missing attribute)
24
+ * - values starting with `[` or `{` are parsed as JSON (invalid JSON is reported in the
25
+ * console and treated as a missing attribute)
26
+ * - every other value is returned as a string
27
+ *
28
+ * @param {HTMLElement} el - The `<json-table>` element
29
+ * @param {keyof JsonTableParams} name - Parameter name (attribute names are case-insensitive)
30
+ * @returns {*} The parsed value, or `undefined` when the attribute is missing or invalid
31
+ *
32
+ * @example
33
+ * // <json-table search="false" cols='[{"key":"id"}]'></json-table>
34
+ * readAttr(el, 'search'); // → false
35
+ * readAttr(el, 'cols'); // → [{ key: 'id' }]
36
+ * readAttr(el, 'perPage'); // → 10 (from perpage="10")
37
+ * readAttr(el, 'caption'); // → undefined
38
+ */
39
+ export function readAttr(el, name) {
40
+
41
+ const attr = el.getAttribute(name);
42
+ if (attr === null) {
43
+ return undefined;
44
+ }
45
+
46
+ const trimmed = attr.trim();
47
+
48
+ if (typeof defaults[name] === 'boolean') {
49
+ const lower = trimmed.toLowerCase();
50
+ // empty string = attribute present without value → true (standard HTML)
51
+ return lower !== 'false' && lower !== '0';
52
+ }
53
+
54
+ if (typeof defaults[name] === 'number') {
55
+ const num = Number(trimmed);
56
+ if (trimmed === '' || Number.isNaN(num)) {
57
+ // eslint-disable-next-line no-console
58
+ console.error(`[json-table] attributo \`${name}\`: valore numerico non valido (${attr}), viene usato il default`);
59
+ return undefined;
60
+ }
61
+ return num;
62
+ }
63
+
64
+ if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
65
+ try {
66
+ return JSON.parse(trimmed);
67
+ } catch (err) {
68
+ // eslint-disable-next-line no-console
69
+ console.error(`[json-table] attributo \`${name}\`: JSON non valido, valore ignorato (viene usato il default)`, err);
70
+ return undefined;
71
+ }
72
+ }
73
+
74
+ return attr;
75
+ }
76
+
77
+
7
78
  /**
8
79
  * Resolves a single parameter, merging the four possible sources.
9
80
  *
10
81
  * Precedence (highest first):
11
82
  * 1. `config` – value passed via `init()`/`reload()`. Present and `!== undefined` always wins;
12
83
  * pass `null` explicitly to skip the HTML attribute and fall back to the defaults.
13
- * 2. HTML attribute – booleans are auto-parsed when the built-in default is a boolean
14
- * (`search`, `search="true"`, `search="1"` → true; `search="false"`, `search="0"` → false);
15
- * values starting with `[` or `{` are parsed as JSON (invalid JSON is reported in the
16
- * console and ignored, falling back to the defaults).
84
+ * 2. HTML attribute (see `readAttr`).
17
85
  * 3. `projectDefaults` – values set via `JsonTable.setDefaults()`.
18
86
  * 4. Built-in default (see `defaults.js`).
19
87
  *
88
+ * Object params listed in `mergedParams` (`classes`, `labels`, `dataTypes`, `serverParams`) are shallow-merged
89
+ * across the sources instead of being replaced, so every source only needs the keys to override.
90
+ *
20
91
  * @param {HTMLElement} el - The `<json-table>` element (source of the HTML attributes)
21
92
  * @param {Partial<JsonTableParams>|null} config - Config passed via `init()` (default: null)
22
93
  * @param {Partial<JsonTableParams>} projectDefaults - Project-wide defaults (default: {})
@@ -24,41 +95,40 @@ import { defaults } from './defaults.js';
24
95
  * @returns {*} The resolved value
25
96
  *
26
97
  * @example
27
- * // <json-table jsonurl="/api/rows.json" search="false"></json-table>
98
+ * // <json-table jsonurl="/api/rows.json" search="false" classes='{"table":"table"}'></json-table>
28
99
  * getParam(el, { jsonUrl: '/api/other.json' }, {}, 'jsonUrl'); // → '/api/other.json' (config)
29
100
  * getParam(el, null, {}, 'search'); // → false (attribute)
30
- * getParam(el, null, { tableClass: 'table' }, 'tableClass'); // → 'table' (project defaults)
31
- * getParam(el, null, {}, 'tableWrapperClass'); // → 'table-responsive' (built-in default)
101
+ * getParam(el, null, { tableId: 'tbl' }, 'tableId'); // → 'tbl' (project defaults)
102
+ * getParam(el, null, {}, 'locale'); // → 'it-IT' (built-in default)
103
+ * getParam(el, { classes: { sortBtn: 'btn' } }, {}, 'classes');
104
+ * // → { ...defaults.classes, table: 'table', sortBtn: 'btn' } (merged)
32
105
  */
33
106
  export function getParam(el, config = null, projectDefaults = {}, name) {
34
107
 
108
+ const configValue = config ? config[name] : undefined;
109
+
110
+ // merged object params: built-in ← project ← attribute ← config
111
+ if (mergedParams.includes(name)) {
112
+ const builtIn = /** @type {Object<string, *>} */ (defaults[name]);
113
+ const projectValue = projectDefaults[name];
114
+ const attrValue = configValue === null ? undefined : readAttr(el, name);
115
+ return {
116
+ ...builtIn,
117
+ ...(isPlainObject(projectValue) ? projectValue : {}),
118
+ ...(isPlainObject(attrValue) ? attrValue : {}),
119
+ ...(isPlainObject(configValue) ? configValue : {})
120
+ };
121
+ }
122
+
35
123
  // 1. config from script (undefined = key not present → go down one level)
36
- if (config && config[name] !== undefined) {
37
- return config[name];
124
+ if (configValue !== undefined) {
125
+ return configValue;
38
126
  }
39
127
 
40
128
  // 2. HTML attribute
41
- const attr = el.getAttribute(name);
42
- if (attr !== null) {
43
- const trimmed = attr.trim();
44
-
45
- if (typeof defaults[name] === 'boolean') {
46
- const lower = trimmed.toLowerCase();
47
- // empty string = attribute present without value → true (standard HTML)
48
- return lower !== 'false' && lower !== '0';
49
- }
50
-
51
- if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
52
- try {
53
- return JSON.parse(trimmed);
54
- } catch (err) {
55
- // eslint-disable-next-line no-console
56
- console.error(`[json-table] attributo \`${name}\`: JSON non valido, valore ignorato (viene usato il default)`, err);
57
- // fall through to the defaults
58
- }
59
- } else {
60
- return attr;
61
- }
129
+ const attrValue = readAttr(el, name);
130
+ if (attrValue !== undefined) {
131
+ return attrValue;
62
132
  }
63
133
 
64
134
  // 3. project defaults
@@ -81,7 +151,7 @@ export function getParam(el, config = null, projectDefaults = {}, name) {
81
151
  *
82
152
  * @example
83
153
  * const params = resolveParams(el, { jsonUrl: '/api/rows.json', debug: true }, {});
84
- * // → { debug: true, jsonUrl: '/api/rows.json', jsonDataField: 'data', data: null, ... }
154
+ * // → { debug: true, jsonUrl: '/api/rows.json', jsonDataField: 'data', data: null, cols: [], ... }
85
155
  */
86
156
  export function resolveParams(el, config = null, projectDefaults = {}) {
87
157
 
@@ -0,0 +1,71 @@
1
+ /*! minimo - json-table: client-side search */
2
+
3
+ /** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
4
+ /** @typedef {import('./parse-rows.js').ParsedRow} ParsedRow */
5
+
6
+ /**
7
+ * Normalizes a search term: trimmed, lowercased, split into words (whitespace separated).
8
+ * @param {string|null|undefined} term
9
+ * @returns {string[]}
10
+ *
11
+ * @example
12
+ * searchWords(' Mario Rossi '); // → ['mario', 'rossi']
13
+ * searchWords(''); // → []
14
+ */
15
+ export function searchWords(term) {
16
+ return String(term ?? '').trim().toLowerCase().split(/\s+/).filter(Boolean);
17
+ }
18
+
19
+
20
+ /**
21
+ * Filters the parsed rows by a search term: every word of the term must be contained in the
22
+ * row `searchText` (precomputed by `parse-rows.js` from the searchable columns; case-insensitive).
23
+ * An empty term returns the original array.
24
+ *
25
+ * @param {ParsedRow[]} rows - Parsed rows
26
+ * @param {string} term - Search term
27
+ * @returns {ParsedRow[]}
28
+ *
29
+ * @example
30
+ * filterRows(state.rows, 'mario 2024'); // rows whose searchText contains both 'mario' and '2024'
31
+ * filterRows(state.rows, ''); // → state.rows
32
+ */
33
+ export function filterRows(rows, term) {
34
+
35
+ const words = searchWords(term);
36
+
37
+ if (!words.length) {
38
+ return rows;
39
+ }
40
+
41
+ return rows.filter(parsed => words.every(word => parsed.searchText.includes(word)));
42
+ }
43
+
44
+
45
+ /**
46
+ * Attaches the `input` listener to the search input (`elements.searchInput`, when present):
47
+ * the search is executed through `jt.setSearch()` after `params.searchDebounce` milliseconds
48
+ * from the last keystroke. The pending timer id is stored in `jt._searchTimer` so that
49
+ * `destroy()` can clear it.
50
+ *
51
+ * @param {JsonTable} jt - The component instance
52
+ * @returns {void}
53
+ *
54
+ * @example
55
+ * setSearchListener(jt); // after mainBuilder(jt)
56
+ */
57
+ export function setSearchListener(jt) {
58
+
59
+ const input = jt.elements.searchInput;
60
+
61
+ if (!input) {
62
+ return;
63
+ }
64
+
65
+ input.addEventListener('input', () => {
66
+ clearTimeout(jt._searchTimer);
67
+ jt._searchTimer = setTimeout(() => {
68
+ jt.setSearch(input.value);
69
+ }, Math.max(0, Number(jt.params.searchDebounce) || 0));
70
+ });
71
+ }