@massimo-cassandro/minimo 1.33.0 → 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.
- package/README.md +2 -1
- package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +1 -1
- package/dev-tools/starter-kit/source_files/app/assets/api_htaccess +20 -0
- package/dev-tools/starter-kit/source_files/{layout-critical.css → app/css/layout.critical.css} +1 -1
- package/dev-tools/starter-kit/source_files/app/css/layout.css +0 -0
- package/dev-tools/starter-kit/source_files/app/index.js +1 -0
- package/dev-tools/starter-kit/source_files/root/webpack-config-modules/inline-critical-css.mjs +41 -0
- package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-setup.mjs +1 -1
- package/dev-tools/starter-kit/{webpack.config.mjs → source_files/root/webpack.config.mjs} +50 -24
- package/dev-tools/starter-kit/starter-install.sh +141 -83
- package/dev-tools/stylelint-config/test/test.module.scss +9 -0
- package/package.json +1 -1
- package/snippets/php/iviewer.php +488 -0
- package/snippets/twig-symfony/ViewerController.php +549 -0
- package/src/custom-properties.css +29 -0
- package/src/minimo.css +3 -0
- package/src/utilities/dom-builder/dom-builder.js +9 -5
- package/src/web-components/json-table/json-table-component.js +387 -38
- package/src/web-components/json-table/json-table-component.module.css +190 -0
- package/src/web-components/json-table/json-table.minimo.tokens.mjs +195 -0
- package/src/web-components/json-table/readme.md +603 -82
- package/src/web-components/json-table/src/cell-content.js +136 -0
- package/src/web-components/json-table/src/content-utils.js +103 -0
- package/src/web-components/json-table/src/data-types.js +280 -0
- package/src/web-components/json-table/src/defaults.js +377 -58
- package/src/web-components/json-table/src/get-data.js +105 -10
- package/src/web-components/json-table/src/icons.js +80 -0
- package/src/web-components/json-table/src/info-section.js +85 -50
- package/src/web-components/json-table/src/main-builder.js +175 -52
- package/src/web-components/json-table/src/pagination.js +253 -0
- package/src/web-components/json-table/src/parse-cols.js +113 -0
- package/src/web-components/json-table/src/parse-rows.js +81 -0
- package/src/web-components/json-table/src/resolve-params.js +102 -32
- package/src/web-components/json-table/src/search.js +71 -0
- package/src/web-components/json-table/src/sorting.js +111 -0
- package/src/web-components/json-table/src/table-body.js +109 -0
- package/src/web-components/json-table/src/table-builder.js +152 -0
- package/src/web-components/json-table/src/table-tfoot.js +56 -0
- package/src/web-components/json-table/src/table-thead.js +183 -0
- package/src/web-components/json-table/src/update-info.js +43 -24
- package/src/web-components/s-datatable-component/{docs.md → readme.md} +5 -3
- package/src/web-components/s-datatable-component/s-datatable-component.js +1 -1
- package/types/global.d.ts +17 -3
- package/dev-tools/starter-kit/source_files/app_index.js +0 -1
- package/snippets/config-templates/_htaccess +0 -69
- package/snippets/config-templates/robots.txt +0 -5
- package/snippets/config-templates/rollup.config.mjs +0 -115
- /package/dev-tools/starter-kit/source_files/{_root_robots.txt → app/assets/robots.txt} +0 -0
- /package/dev-tools/starter-kit/source_files/{_root_htaccess → app/assets/root_htaccess} +0 -0
- /package/{src → dev-tools/starter-kit/source_files/app/css}/fonts.css +0 -0
- /package/dev-tools/starter-kit/source_files/{favicons-readme.md → app/favicons/favicons-readme.md} +0 -0
- /package/dev-tools/starter-kit/{webpack-template.ejs → source_files/app/tpl/index.ejs} +0 -0
- /package/dev-tools/starter-kit/source_files/{__project__.code-workspace → root/__project__.code-workspace} +0 -0
- /package/dev-tools/starter-kit/source_files/{_browserslistrc → root/_browserslistrc} +0 -0
- /package/dev-tools/starter-kit/source_files/{_editorconfig → root/_editorconfig} +0 -0
- /package/dev-tools/starter-kit/source_files/{_gitignore → root/_gitignore} +0 -0
- /package/dev-tools/starter-kit/source_files/{_prettierrc → root/_prettierrc} +0 -0
- /package/dev-tools/starter-kit/source_files/{eslint.config.mjs → root/eslint.config.mjs} +0 -0
- /package/dev-tools/starter-kit/source_files/{jsconfig.json → root/jsconfig.json} +0 -0
- /package/dev-tools/starter-kit/{package-tpl.json → source_files/root/package-tpl.json} +0 -0
- /package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/css-rules.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/get-jsConfig-aliases.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/mini-svg-data-uri-loader.cjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/postcss.config.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-variables-safelist.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svg-rules.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svgo.config.mjs +0 -0
- /package/snippets/{config-templates/_github → _github}/dependabot.yml +0 -0
- /package/snippets/{error-pages/error-pages.js → error-pages.js} +0 -0
- /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.jsx +0 -0
- /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.module.scss +0 -0
- /package/snippets/{config-templates → root}/_hintrc.cjs +0 -0
- /package/snippets/{config-templates → root}/_npmignore +0 -0
- /package/snippets/{config-templates → root}/_npmrc +0 -0
- /package/snippets/{config-templates → root}/_stylelintignore +0 -0
- /package/snippets/{config-templates → root}/babel.config--react.json +0 -0
- /package/snippets/{config-templates → root}/babel.config--solidJS.json +0 -0
- /package/snippets/{ImgViewerController.php → twig-symfony/ImgViewerController.php} +0 -0
- /package/snippets/{UnsplashController.php → twig-symfony/UnsplashController.php} +0 -0
- /package/snippets/{config-templates → twig-symfony}/_env.local +0 -0
- /package/snippets/{error-pages → twig-symfony}/bundles/TwigBundle/Exception/error.html.twig +0 -0
- /package/snippets/{flash-messages.html.twig → twig-symfony/flash-messages.html.twig} +0 -0
- /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
|
+
}
|