@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,136 @@
1
+ /*! minimo - json-table: cell & footer content */
2
+
3
+ import { getNestedValue, resolveMustache } from './content-utils.js';
4
+ import { toNumber } from './data-types.js';
5
+
6
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
7
+ /** @typedef {import('./defaults.js').CellContent} CellContent */
8
+ /** @typedef {import('./parse-cols.js').ParsedCol} ParsedCol */
9
+
10
+ /**
11
+ * Computes the content of a body cell.
12
+ *
13
+ * Pipeline:
14
+ * 1. column `render`: function `(row, tr, td)` or mustache-like string (`[[key]]`); a function
15
+ * returning `undefined` lets the pipeline go on (useful to only decorate `td`/`tr`), `null`
16
+ * gives `renderNullAs`
17
+ * 2. `null`/`undefined` raw value → `renderNullAs`
18
+ * 3. data type `render(value, row, params)`
19
+ * 4. raw value
20
+ *
21
+ * @param {ParsedCol} col - Parsed column
22
+ * @param {Object} row - Row object
23
+ * @param {JsonTableParams} params - Resolved params
24
+ * @param {HTMLTableRowElement} [tr] - Row element, passed to the column `render`
25
+ * @param {HTMLTableCellElement} [td] - Cell element, passed to the column `render`
26
+ * @returns {CellContent}
27
+ *
28
+ * @example
29
+ * cellContent({ ...col, key: 'amount', _type: types.euro }, { amount: 12 }, params); // → '12,00 €'
30
+ * cellContent({ ...col, key: 'amount', _type: types.euro }, { amount: null }, params); // → '—' (renderNullAs)
31
+ * cellContent({ ...col, render: '<b>[[name]]</b>' }, { name: 'Mario' }, params); // → '<b>Mario</b>'
32
+ * cellContent({ ...col, render: (row, tr, td) => { td.title = row.name; } }, row, params, tr, td);
33
+ * // → data type / raw value (render returned undefined)
34
+ */
35
+ export function cellContent(col, row, params, tr, td) {
36
+
37
+ const nullAs = params.renderNullAs ?? '';
38
+
39
+ if (col.render != null) {
40
+ const content = typeof col.render === 'function'
41
+ ? col.render(row, tr, td)
42
+ : resolveMustache(String(col.render), row, nullAs);
43
+
44
+ if (content !== undefined) {
45
+ return content ?? nullAs;
46
+ }
47
+ }
48
+
49
+ const value = getNestedValue(row, col.key);
50
+
51
+ if (value == null) {
52
+ return nullAs;
53
+ }
54
+
55
+ if (typeof col._type.render === 'function') {
56
+ return col._type.render(value, row, params) ?? nullAs;
57
+ }
58
+
59
+ return value;
60
+ }
61
+
62
+
63
+ /**
64
+ * Built-in footer aggregates (`tfootRender: '@sum'`, ...), computed on the numeric values
65
+ * of the column (non-numeric values are skipped).
66
+ * @type {Object<string, (nums: number[]) => number>}
67
+ */
68
+ const aggregates = {
69
+ '@sum': nums => nums.reduce((acc, n) => acc + n, 0),
70
+ '@avg': nums => (nums.length ? nums.reduce((acc, n) => acc + n, 0) / nums.length : NaN),
71
+ '@min': nums => (nums.length ? Math.min(...nums) : NaN),
72
+ '@max': nums => (nums.length ? Math.max(...nums) : NaN),
73
+ '@count': nums => nums.length
74
+ };
75
+
76
+
77
+ /**
78
+ * Computes the content of a footer cell from the column `tfootRender`:
79
+ * - function `(rows, td) => content`
80
+ * - built-in aggregate `'@sum'`, `'@avg'`, `'@min'`, `'@max'` (formatted by the column data
81
+ * type, e.g. as currency for `euro` columns) or `'@count'` (number of numeric values)
82
+ * - any other string: static content (e.g. a "Totale" label)
83
+ * - `null`: empty cell
84
+ *
85
+ * @param {ParsedCol} col - Parsed column
86
+ * @param {Array<Object>} rows - Rows to aggregate (filtered set or current page, see `updateFooterOnPageChange`)
87
+ * @param {JsonTableParams} params - Resolved params
88
+ * @param {HTMLTableCellElement} [td] - Footer cell element, passed to the `tfootRender` function
89
+ * @returns {CellContent}
90
+ *
91
+ * @example
92
+ * tfootContent({ ...col, key: 'amount', tfootRender: '@sum', _type: types.euro }, rows, params); // → '1.234,56 €'
93
+ * tfootContent({ ...col, tfootRender: 'Totale' }, rows, params); // → 'Totale'
94
+ * tfootContent({ ...col, tfootRender: rows => `${rows.length} righe` }, rows, params); // → '10 righe'
95
+ * tfootContent({ ...col, tfootRender: null }, rows, params); // → ''
96
+ */
97
+ export function tfootContent(col, rows, params, td) {
98
+
99
+ const renderer = col.tfootRender;
100
+
101
+ if (renderer == null) {
102
+ return '';
103
+ }
104
+
105
+ if (typeof renderer === 'function') {
106
+ return renderer(rows, td) ?? '';
107
+ }
108
+
109
+ const tpl = String(renderer);
110
+
111
+ if (tpl.startsWith('@')) {
112
+ const aggregate = aggregates[tpl];
113
+
114
+ if (!aggregate) {
115
+ // eslint-disable-next-line no-console
116
+ console.error(`[json-table] colonna \`${col.key}\`: aggregato \`${tpl}\` non riconosciuto (disponibili: ${Object.keys(aggregates).join(', ')})`);
117
+ return '';
118
+ }
119
+
120
+ const nums = rows
121
+ .map(row => toNumber(getNestedValue(row, col.key)))
122
+ .filter(n => !Number.isNaN(n));
123
+
124
+ const result = aggregate(nums);
125
+
126
+ if (tpl === '@count') {
127
+ return result.toLocaleString(params.locale);
128
+ }
129
+
130
+ return typeof col._type.render === 'function'
131
+ ? col._type.render(result, null, params)
132
+ : result.toLocaleString(params.locale, params.numbersLocaleOpts);
133
+ }
134
+
135
+ return tpl;
136
+ }
@@ -0,0 +1,103 @@
1
+ /*! minimo - json-table: content utilities */
2
+
3
+ import { domBuilder } from '../../../utilities/dom-builder/dom-builder.js';
4
+
5
+ /** @typedef {import('./defaults.js').CellContent} CellContent */
6
+
7
+ /**
8
+ * Replaces the content of `el` with `content`, accepting every content form used by the
9
+ * component (see `CellContent`): a Node is appended as-is, a domBuilder array is built inside
10
+ * the element, a string/number is set as text or, when it contains `<`, as sanitized markup
11
+ * (`Element.setHTML` where supported, `innerHTML` otherwise, consistently with domBuilder).
12
+ * The empty sanitizer config passed to `setHTML` keeps `class`, `id`, `style` and `data-*`
13
+ * attributes, which the browser default configuration removes, while unsafe content (scripts,
14
+ * event handlers, `javascript:` URLs) is always stripped.
15
+ * A function is invoked and its result used. `null`/`undefined` empty the element.
16
+ *
17
+ * @param {HTMLElement} el - Target element (emptied first)
18
+ * @param {CellContent|(() => CellContent)} content - Content to set
19
+ * @returns {void}
20
+ *
21
+ * @example
22
+ * setContent(td, 'plain text');
23
+ * setContent(td, 'a <strong>bold</strong> text');
24
+ * setContent(td, 1234);
25
+ * setContent(td, document.createElement('span'));
26
+ * setContent(td, [{ tag: 'a', attrs: { href: '#' }, content: 'link' }]);
27
+ * setContent(td, null); // → empty cell
28
+ */
29
+ export function setContent(el, content) {
30
+
31
+ const resolved = typeof content === 'function' ? content() : content;
32
+
33
+ if (resolved == null) {
34
+ el.replaceChildren();
35
+ return;
36
+ }
37
+
38
+ if (resolved instanceof Node) {
39
+ el.replaceChildren(resolved);
40
+ return;
41
+ }
42
+
43
+ if (Array.isArray(resolved)) {
44
+ domBuilder(resolved, el, { emptyParent: true });
45
+ return;
46
+ }
47
+
48
+ const text = String(resolved);
49
+
50
+ if (!text.includes('<')) {
51
+ el.textContent = text;
52
+
53
+ } else if (typeof el.setHTML === 'function') {
54
+ el.setHTML(text, { sanitizer: {} });
55
+
56
+ } else {
57
+ el.innerHTML = text;
58
+ }
59
+ }
60
+
61
+
62
+ /**
63
+ * Reads a value from an object through a dot-separated key path.
64
+ * Returns `undefined` when an intermediate key does not exist.
65
+ *
66
+ * @param {Object|null|undefined} obj - Source object
67
+ * @param {string} path - Key or dot-separated path
68
+ * @returns {*}
69
+ *
70
+ * @example
71
+ * getNestedValue({ owner: { name: 'Mario' } }, 'owner.name'); // → 'Mario'
72
+ * getNestedValue({ name: 'Mario' }, 'name'); // → 'Mario'
73
+ * getNestedValue({ name: 'Mario' }, 'owner.name'); // → undefined
74
+ */
75
+ export function getNestedValue(obj, path) {
76
+ return path.split('.').reduce(
77
+ (acc, key) => (acc == null ? undefined : /** @type {Object<string, *>} */ (acc)[key]),
78
+ /** @type {*} */ (obj)
79
+ );
80
+ }
81
+
82
+
83
+ /**
84
+ * Resolves a mustache-like template: every `[[key]]` placeholder is replaced with the
85
+ * corresponding row value (nested keys allowed); placeholders resolving to `null`/`undefined`
86
+ * are replaced with `nullAs`. Useful for the js-free configuration via HTML attributes.
87
+ *
88
+ * @param {string} tpl - Template string
89
+ * @param {Object} row - Row object
90
+ * @param {string} [nullAs=''] - Replacement of null/undefined values (default: '')
91
+ * @returns {string}
92
+ *
93
+ * @example
94
+ * resolveMustache('<a href="/users/[[id]]">[[name]]</a>', { id: 12, name: 'Mario' });
95
+ * // → '<a href="/users/12">Mario</a>'
96
+ * resolveMustache('[[owner.name]]', { owner: null }, '—'); // → '—'
97
+ */
98
+ export function resolveMustache(tpl, row, nullAs = '') {
99
+ return tpl.replace(/\[\[(.*?)\]\]/g, (_, key) => {
100
+ const value = getNestedValue(row, key.trim());
101
+ return value == null ? nullAs : String(value);
102
+ });
103
+ }
@@ -0,0 +1,280 @@
1
+ /*! minimo - json-table: data types */
2
+
3
+ import * as styles from '../json-table-component.module.css';
4
+ import { classnames } from '../../../utilities/classnames.js';
5
+ import { iconContent } from './icons.js';
6
+
7
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
8
+ /** @typedef {import('./defaults.js').DataTypeDefinition} DataTypeDefinition */
9
+
10
+ /**
11
+ * Converts a value to a number; empty strings and nullish values give `NaN`.
12
+ * @param {*} value
13
+ * @returns {number}
14
+ *
15
+ * @example
16
+ * toNumber('12.5'); // → 12.5
17
+ * toNumber(''); // → NaN
18
+ * toNumber(null); // → NaN
19
+ */
20
+ export function toNumber(value) {
21
+ if (value == null || value === '' || typeof value === 'boolean') {
22
+ return NaN;
23
+ }
24
+ return typeof value === 'number' ? value : Number(value);
25
+ }
26
+
27
+
28
+ /**
29
+ * Converts a boolean-like value (`true`/`false`, `1`/`0`, `'1'`/`'0'`, `'true'`/`'false'`)
30
+ * to a boolean; every other value gives `null`, so that `null` can be told apart from `false`.
31
+ * @param {*} value
32
+ * @returns {boolean|null}
33
+ *
34
+ * @example
35
+ * toBool(1); // → true
36
+ * toBool('false'); // → false
37
+ * toBool(null); // → null
38
+ * toBool('abc'); // → null
39
+ */
40
+ export function toBool(value) {
41
+ if (value === true || value === 1 || value === '1' || value === 'true') {
42
+ return true;
43
+ }
44
+ if (value === false || value === 0 || value === '0' || value === 'false') {
45
+ return false;
46
+ }
47
+ return null;
48
+ }
49
+
50
+
51
+ /**
52
+ * Parses a date-like value: ISO string, timestamp, `Date` object or a Symfony-like object
53
+ * (`{ date: '2022-12-02 06:18:55', timezone_type: 3, timezone: 'Europe/Berlin' }`).
54
+ * TODO fuso orario degli oggetti Symfony non gestito (la stringa `date` è interpretata come ora locale)
55
+ * @param {*} value
56
+ * @returns {Date|null} `null` for empty or invalid values
57
+ *
58
+ * @example
59
+ * parseDate('2024-01-31'); // → Date
60
+ * parseDate({ date: '2024-01-31 01:33:10' }); // → Date
61
+ * parseDate(''); // → null
62
+ */
63
+ export function parseDate(value) {
64
+ if (value == null || value === '') {
65
+ return null;
66
+ }
67
+ let date;
68
+ if (value instanceof Date) {
69
+ date = value;
70
+ } else if (typeof value === 'object' && Object.hasOwn(value, 'date')) {
71
+ date = new Date(String(value.date).replace(' ', 'T'));
72
+ } else {
73
+ date = new Date(value);
74
+ }
75
+ return Number.isNaN(date.getTime()) ? null : date;
76
+ }
77
+
78
+
79
+ /**
80
+ * Formats a numeric value, applying `renderNaNAs` and `renderZeroAs`.
81
+ * @param {*} value - Raw value
82
+ * @param {JsonTableParams} params - Resolved params
83
+ * @param {Intl.NumberFormatOptions} formatOpts - `toLocaleString` options
84
+ * @param {(formatted: string, num: number) => string} [wrap] - Optional decorator of the formatted string
85
+ * @returns {string}
86
+ *
87
+ * @example
88
+ * formatNumber(1529.42, params, { maximumFractionDigits: 2 }); // → '1.529,42' (it-IT)
89
+ * formatNumber('abc', params, {}); // → params.renderNaNAs ('—')
90
+ * formatNumber(0, { ...params, renderZeroAs: '-' }, {}); // → '-'
91
+ * formatNumber(4.63, params, {}, s => `${s}\u202F<small>%</small>`); // → '4,63 <small>%</small>'
92
+ */
93
+ export function formatNumber(value, params, formatOpts, wrap) {
94
+ const num = toNumber(value);
95
+ if (Number.isNaN(num)) {
96
+ return params.renderNaNAs ?? '';
97
+ }
98
+ if (num === 0 && params.renderZeroAs != null) {
99
+ return params.renderZeroAs;
100
+ }
101
+ const formatted = num.toLocaleString(params.locale, formatOpts);
102
+ return wrap ? wrap(formatted, num) : formatted;
103
+ }
104
+
105
+
106
+ /**
107
+ * Built-in data types, built on the resolved params (class names, locale options, icons).
108
+ *
109
+ * | key | description | cell classes |
110
+ * |---------------|-----------------------------------------------------------------|-------------------------|
111
+ * | `string` | default type, value as-is | – |
112
+ * | `num` | number formatted with `numbersLocaleOpts` | textEnd numeric nowrap |
113
+ * | `id` | numeric id: raw value, right aligned, not searchable | textEnd numeric |
114
+ * | `perc` | percentage already in 0–100 scale, `currPercLocaleOpts` + " %" | textEnd numeric nowrap |
115
+ * | `percDecimal` | percentage in 0–1 scale (multiplied by 100) | textEnd numeric nowrap |
116
+ * | `currency` | `Intl` currency format, `params.currency` | textEnd numeric nowrap |
117
+ * | `euro` | as `currency`, EUR forced | textEnd numeric nowrap |
118
+ * | `date` | `<time>` element, `datesLocaleOpts` | textEnd nowrap |
119
+ * | `datetime` | as `date`, plus time part (`timesLocaleOpts`) | textEnd nowrap |
120
+ * | `bool` | `boolTrueIcon`/`boolFalseIcon`; not sortable/searchable | (internal icon styles) |
121
+ * | `email` | soft line breaks around `@` | – |
122
+ *
123
+ * @param {JsonTableParams} params - Resolved params
124
+ * @returns {Object<string, DataTypeDefinition>}
125
+ *
126
+ * @example
127
+ * const types = builtInDataTypes(params);
128
+ * types.num.render(1529.42, row, params); // → '1.529,42'
129
+ */
130
+ export function builtInDataTypes(params) {
131
+
132
+ const c = params.classes;
133
+ const numericClass = classnames(c.textEnd, c.numeric, c.nowrap);
134
+ const dateClass = classnames(c.textEnd, c.nowrap);
135
+
136
+ /** @type {(formatOpts: Intl.NumberFormatOptions, wrap?: (formatted: string, num: number) => string) => DataTypeDefinition} */
137
+ const numericType = (formatOpts, wrap) => ({
138
+ headerClass: numericClass,
139
+ cellClass: numericClass,
140
+ render: (value, row, p) => formatNumber(value, p, formatOpts, wrap),
141
+ sortValue: value => toNumber(value),
142
+ searchValue: (value, row, p) => {
143
+ const num = toNumber(value);
144
+ return Number.isNaN(num) ? '' : num.toLocaleString(p.locale, formatOpts);
145
+ }
146
+ });
147
+
148
+ // TODO implementare sfTime / sfDatetime (??)
149
+ /** @type {(withTime: boolean) => DataTypeDefinition} */
150
+ const dateType = withTime => ({
151
+ headerClass: dateClass,
152
+ cellClass: dateClass,
153
+ render: (value, row, p) => {
154
+ const date = parseDate(value);
155
+ if (!date) {
156
+ return p.renderNaNAs ?? '';
157
+ }
158
+ const iso = date.toISOString();
159
+ return `<time datetime="${withTime ? iso : iso.substring(0, 10)}">` +
160
+ `<span class="${c.nowrap}">` + date.toLocaleString(p.locale, p.datesLocaleOpts) + '</span>' +
161
+ (withTime ? ' <small>' + date.toLocaleString(p.locale, p.timesLocaleOpts) + '</small>' : '') +
162
+ '</time>';
163
+ },
164
+ sortValue: value => parseDate(value)?.getTime() ?? null,
165
+ searchValue: (value, row, p) => {
166
+ const date = parseDate(value);
167
+ return date
168
+ ? date.toLocaleString(p.locale, { ...p.datesLocaleOpts, ...(withTime ? p.timesLocaleOpts : {}) })
169
+ : '';
170
+ }
171
+ });
172
+
173
+ const percWrap = /** @type {(formatted: string) => string} */ (formatted => `${formatted}\u202F<small>%</small>`);
174
+
175
+ return {
176
+
177
+ string: {},
178
+
179
+ num: numericType(params.numbersLocaleOpts),
180
+
181
+ id: {
182
+ headerClass: classnames(c.textEnd, c.numeric),
183
+ cellClass: classnames(c.textEnd, c.numeric),
184
+ sortValue: value => toNumber(value),
185
+ colDefaults: { searchable: false }
186
+ },
187
+
188
+ perc: numericType(params.currPercLocaleOpts, percWrap),
189
+
190
+ percDecimal: {
191
+ ...numericType(params.currPercLocaleOpts, percWrap),
192
+ render: (value, row, p) => formatNumber(toNumber(value) * 100, p, p.currPercLocaleOpts, percWrap)
193
+ },
194
+
195
+ currency: numericType({ style: 'currency', currency: params.currency, ...params.currPercLocaleOpts }),
196
+
197
+ euro: numericType({ style: 'currency', currency: 'EUR', ...params.currPercLocaleOpts }),
198
+
199
+ date: dateType(false),
200
+
201
+ datetime: dateType(true),
202
+
203
+ bool: {
204
+ headerClass: c.textCenter,
205
+ cellClass: null,
206
+ internalCellClass: value => {
207
+ const bool = toBool(value);
208
+ return classnames(
209
+ styles.boolCell, c.boolCell,
210
+ bool === true && [styles.boolTrue, c.boolTrue],
211
+ bool === false && [styles.boolFalse, c.boolFalse]
212
+ );
213
+ },
214
+ render: (value, row, p) => {
215
+ const bool = toBool(value);
216
+ if (bool === true) {
217
+ return iconContent(p.boolTrueIcon);
218
+ }
219
+ if (bool === false) {
220
+ return iconContent(p.boolFalseIcon);
221
+ }
222
+ return p.renderNullAs ?? '';
223
+ },
224
+ sortValue: value => {
225
+ const bool = toBool(value);
226
+ return bool === null ? -1 : (bool ? 1 : 0);
227
+ },
228
+ searchValue: () => '',
229
+ colDefaults: { sortable: false, searchable: false }
230
+ },
231
+
232
+ email: {
233
+ render: value => String(value).replace('@', '<wbr>@<wbr>')
234
+ }
235
+ };
236
+ }
237
+
238
+
239
+ /**
240
+ * Merges the custom data types (`params.dataTypes`) with the built-in ones.
241
+ *
242
+ * A custom key matching a built-in type overrides only the given properties; a new key can
243
+ * extend a built-in type via `inheritsFrom`.
244
+ *
245
+ * @param {JsonTableParams} params - Resolved params
246
+ * @returns {Object<string, DataTypeDefinition>} The complete data types map
247
+ * @throws {Error} When `inheritsFrom` refers to an unknown type
248
+ *
249
+ * @example
250
+ * const types = buildDataTypes({
251
+ * ...params,
252
+ * dataTypes: {
253
+ * num: { cellClass: 'text-end fw-bold' }, // overrides only `cellClass` of the built-in `num`
254
+ * km: { inheritsFrom: 'num', render: v => `${v} km` } // new type based on `num`
255
+ * }
256
+ * });
257
+ */
258
+ export function buildDataTypes(params) {
259
+
260
+ const builtIn = builtInDataTypes(params);
261
+ const result = { ...builtIn };
262
+
263
+ for (const [key, def] of Object.entries(params.dataTypes ?? {})) {
264
+
265
+ if (def == null || typeof def !== 'object') {
266
+ continue;
267
+ }
268
+
269
+ const baseKey = builtIn[key] ? key : def.inheritsFrom;
270
+ const base = baseKey ? result[baseKey] : null;
271
+
272
+ if (def.inheritsFrom && !base) {
273
+ throw new Error(`[json-table] dataType \`${key}\`: \`inheritsFrom\` fa riferimento a un tipo inesistente (${def.inheritsFrom})`);
274
+ }
275
+
276
+ result[key] = { ...(base ?? {}), ...def };
277
+ }
278
+
279
+ return result;
280
+ }