@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,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
+ }
@@ -0,0 +1,166 @@
1
+ /*! minimo - json-table: params resolution */
2
+
3
+ import { defaults, mergedParams } from './defaults.js';
4
+
5
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
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
+
78
+ /**
79
+ * Resolves a single parameter, merging the four possible sources.
80
+ *
81
+ * Precedence (highest first):
82
+ * 1. `config` – value passed via `init()`/`reload()`. Present and `!== undefined` always wins;
83
+ * pass `null` explicitly to skip the HTML attribute and fall back to the defaults.
84
+ * 2. HTML attribute (see `readAttr`).
85
+ * 3. `projectDefaults` – values set via `JsonTable.setDefaults()`.
86
+ * 4. Built-in default (see `defaults.js`).
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
+ *
91
+ * @param {HTMLElement} el - The `<json-table>` element (source of the HTML attributes)
92
+ * @param {Partial<JsonTableParams>|null} config - Config passed via `init()` (default: null)
93
+ * @param {Partial<JsonTableParams>} projectDefaults - Project-wide defaults (default: {})
94
+ * @param {keyof JsonTableParams} name - Parameter name
95
+ * @returns {*} The resolved value
96
+ *
97
+ * @example
98
+ * // <json-table jsonurl="/api/rows.json" search="false" classes='{"table":"table"}'></json-table>
99
+ * getParam(el, { jsonUrl: '/api/other.json' }, {}, 'jsonUrl'); // → '/api/other.json' (config)
100
+ * getParam(el, null, {}, 'search'); // → false (attribute)
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)
105
+ */
106
+ export function getParam(el, config = null, projectDefaults = {}, name) {
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
+
123
+ // 1. config from script (undefined = key not present → go down one level)
124
+ if (configValue !== undefined) {
125
+ return configValue;
126
+ }
127
+
128
+ // 2. HTML attribute
129
+ const attrValue = readAttr(el, name);
130
+ if (attrValue !== undefined) {
131
+ return attrValue;
132
+ }
133
+
134
+ // 3. project defaults
135
+ if (projectDefaults[name] !== undefined) {
136
+ return projectDefaults[name];
137
+ }
138
+
139
+ // 4. built-in default
140
+ return defaults[name];
141
+ }
142
+
143
+
144
+ /**
145
+ * Resolves every parameter defined in `defaults.js` (see `getParam` for the precedence rules).
146
+ *
147
+ * @param {HTMLElement} el - The `<json-table>` element
148
+ * @param {Partial<JsonTableParams>|null} [config=null] - Config passed via `init()` (default: null)
149
+ * @param {Partial<JsonTableParams>} [projectDefaults={}] - Project-wide defaults (default: {})
150
+ * @returns {JsonTableParams} The complete params object
151
+ *
152
+ * @example
153
+ * const params = resolveParams(el, { jsonUrl: '/api/rows.json', debug: true }, {});
154
+ * // → { debug: true, jsonUrl: '/api/rows.json', jsonDataField: 'data', data: null, cols: [], ... }
155
+ */
156
+ export function resolveParams(el, config = null, projectDefaults = {}) {
157
+
158
+ const params = Object.fromEntries(
159
+ Object.keys(defaults).map(name => [
160
+ name,
161
+ getParam(el, config, projectDefaults, /** @type {keyof JsonTableParams} */ (name))
162
+ ])
163
+ );
164
+
165
+ return /** @type {JsonTableParams} */ (params);
166
+ }
@@ -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
+ }
@@ -0,0 +1,111 @@
1
+ /*! minimo - json-table: client-side sorting */
2
+
3
+ /** @typedef {import('./parse-rows.js').ParsedRow} ParsedRow */
4
+ /** @typedef {'asc'|'desc'} SortDir */
5
+
6
+ /**
7
+ * Whether a sort value counts as "empty" (sorted last in both directions).
8
+ * @param {*} value
9
+ * @returns {boolean}
10
+ */
11
+ function isEmpty(value) {
12
+ return value == null || value === '' || (typeof value === 'number' && Number.isNaN(value));
13
+ }
14
+
15
+ /**
16
+ * Whether a value is a number or a numeric string.
17
+ * @param {*} value
18
+ * @returns {boolean}
19
+ */
20
+ function isNumeric(value) {
21
+ if (typeof value === 'number') {
22
+ return !Number.isNaN(value);
23
+ }
24
+ return typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value));
25
+ }
26
+
27
+
28
+ /**
29
+ * Compares two sort values in ascending order: empty values (`null`, `undefined`, `''`, `NaN`)
30
+ * always last, numbers (and numeric strings, when both are numeric) numerically, booleans as
31
+ * 0/1, everything else as strings via `localeCompare` (numeric collation, accent/case insensitive).
32
+ *
33
+ * @param {*} a
34
+ * @param {*} b
35
+ * @param {string} [locale] - Locale used by `localeCompare` (default: browser default)
36
+ * @returns {number} Negative when `a` comes first, positive when `b` comes first, 0 when equal
37
+ *
38
+ * @example
39
+ * compareValues(2, 10); // → negative
40
+ * compareValues('file10', 'file2'); // → positive (numeric collation)
41
+ * compareValues('à', 'b', 'it-IT'); // → negative
42
+ * compareValues(null, 'a'); // → positive (empty values last)
43
+ */
44
+ export function compareValues(a, b, locale) {
45
+
46
+ const emptyA = isEmpty(a);
47
+ const emptyB = isEmpty(b);
48
+
49
+ if (emptyA || emptyB) {
50
+ return emptyA === emptyB ? 0 : (emptyA ? 1 : -1);
51
+ }
52
+
53
+ if (typeof a === 'boolean' && typeof b === 'boolean') {
54
+ return Number(a) - Number(b);
55
+ }
56
+
57
+ if (isNumeric(a) && isNumeric(b)) {
58
+ return Number(a) - Number(b);
59
+ }
60
+
61
+ return String(a).localeCompare(String(b), locale, { numeric: true, sensitivity: 'base' });
62
+ }
63
+
64
+
65
+ /**
66
+ * Returns a sorted copy of the parsed rows, by the precomputed `sortValues[key]` of every row.
67
+ * The sort is stable, so rows with equal values keep their original order.
68
+ *
69
+ * @param {ParsedRow[]} rows - Parsed rows (see `parse-rows.js`)
70
+ * @param {string} key - Column key
71
+ * @param {SortDir} dir - Direction
72
+ * @param {string} [locale] - Locale used to compare strings
73
+ * @returns {ParsedRow[]} A new array
74
+ *
75
+ * @example
76
+ * sortRows(state.rows, 'amount', 'desc', 'it-IT');
77
+ */
78
+ export function sortRows(rows, key, dir, locale) {
79
+
80
+ const sign = dir === 'desc' ? -1 : 1;
81
+
82
+ return [...rows].sort((a, b) => {
83
+ const va = a.sortValues[key];
84
+ const vb = b.sortValues[key];
85
+ const cmp = compareValues(va, vb, locale);
86
+ // empty values stay last regardless of the direction
87
+ return (isEmpty(va) || isEmpty(vb)) ? cmp : cmp * sign;
88
+ });
89
+ }
90
+
91
+
92
+ /**
93
+ * Next direction in the sort cycle of a column button: none → `asc` → `desc` → none.
94
+ *
95
+ * @param {SortDir|null|undefined} current - Current direction of the column (null = not sorted)
96
+ * @returns {SortDir|null}
97
+ *
98
+ * @example
99
+ * nextSortDir(null); // → 'asc'
100
+ * nextSortDir('asc'); // → 'desc'
101
+ * nextSortDir('desc'); // → null
102
+ */
103
+ export function nextSortDir(current) {
104
+ if (current === 'asc') {
105
+ return 'desc';
106
+ }
107
+ if (current === 'desc') {
108
+ return null;
109
+ }
110
+ return 'asc';
111
+ }
@@ -0,0 +1,109 @@
1
+ /*! minimo - json-table: tbody rendering */
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 { setContent, getNestedValue } from './content-utils.js';
7
+ import { cellContent } from './cell-content.js';
8
+
9
+ /** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
10
+ /** @typedef {import('./parse-rows.js').ParsedRow} ParsedRow */
11
+
12
+ /**
13
+ * domBuilder item of a body row.
14
+ *
15
+ * Cells are created empty by domBuilder (tag, classes, attributes) and filled in the `<tr>`
16
+ * callback, so that the column `render` functions receive both the `tr` and the `td` elements.
17
+ * `trCallback` is invoked last.
18
+ *
19
+ * Generated structure:
20
+ * ```
21
+ * tr[data-jt-idx]
22
+ * td[data-key][cellClass][internalCellClass] ← cellContent()
23
+ * th[scope=row][data-key]… ← when col.rowHeading is true
24
+ * ```
25
+ *
26
+ * @param {ParsedRow} parsed - Parsed row (see `parse-rows.js`)
27
+ * @param {JsonTable} jt - The component instance (`params`, `cols`)
28
+ * @returns {DomBuilderItem}
29
+ *
30
+ * @example
31
+ * domBuilder([rowItem(parsedRow, jt)], tbody);
32
+ */
33
+ export function rowItem(parsed, jt) {
34
+
35
+ const { params, cols } = jt;
36
+ const row = parsed.row;
37
+
38
+ return {
39
+ tag: 'tr',
40
+ attrs: { 'data-jt-idx': parsed.idx },
41
+ children: cols.map(col => ({
42
+ tag: col.rowHeading ? 'th' : 'td',
43
+ className: classnames(
44
+ col.cellClass,
45
+ typeof col._type.internalCellClass === 'function'
46
+ ? col._type.internalCellClass(getNestedValue(row, col.key), row, params)
47
+ : null
48
+ ),
49
+ attrs: {
50
+ scope: col.rowHeading ? 'row' : null,
51
+ 'data-key': col.key
52
+ }
53
+ })),
54
+ callback: el => {
55
+ const tr = /** @type {HTMLTableRowElement} */ (el);
56
+ const cells = tr.cells;
57
+
58
+ cols.forEach((col, i) => {
59
+ const td = cells[i];
60
+ setContent(td, cellContent(col, row, params, tr, td));
61
+ });
62
+
63
+ if (typeof params.trCallback === 'function') {
64
+ params.trCallback(tr, row, params);
65
+ }
66
+ }
67
+ };
68
+ }
69
+
70
+
71
+ /**
72
+ * Renders the rows of the current page (`state.pageRows`) inside `elements.tbody`, replacing
73
+ * the previous content. When there are no rows, a single cell spanning every column shows
74
+ * `labels.noResults` (search active) or `labels.noRows`.
75
+ *
76
+ * @param {JsonTable} jt - The component instance
77
+ * @returns {void}
78
+ *
79
+ * @example
80
+ * renderTbody(jt); // after `jt.state` has been updated
81
+ */
82
+ export function renderTbody(jt) {
83
+
84
+ const { params, elements, state, cols } = jt;
85
+ const tbody = elements.tbody;
86
+
87
+ if (!tbody) {
88
+ return;
89
+ }
90
+
91
+ if (!state.pageRows.length) {
92
+ domBuilder([
93
+ {
94
+ tag: 'tr',
95
+ children: [
96
+ {
97
+ tag: 'td',
98
+ className: classnames(styles.empty, params.classes.empty),
99
+ attrs: { colspan: Math.max(cols.length, 1) },
100
+ content: state.searchTerm ? params.labels.noResults : params.labels.noRows
101
+ }
102
+ ]
103
+ }
104
+ ], tbody, { emptyParent: true });
105
+ return;
106
+ }
107
+
108
+ domBuilder(state.pageRows.map(parsed => rowItem(parsed, jt)), tbody, { emptyParent: true });
109
+ }
@@ -0,0 +1,152 @@
1
+ /*! minimo - json-table: table part (table, caption and footer bar) */
2
+
3
+ import * as styles from '../json-table-component.module.css';
4
+ import { classnames } from '../../../utilities/classnames.js';
5
+ import { theadPart } from './table-thead.js';
6
+
7
+ /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
8
+ /** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
9
+ /** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
10
+
11
+ /** Counter used to generate unique caption ids */
12
+ let captionUid = 0;
13
+
14
+ /**
15
+ * Id of the caption element of a table (used by `aria-labelledby`): `<tableId>-caption`, or a
16
+ * generated unique id when `tableId` is not set.
17
+ *
18
+ * @param {JsonTableParams} params - Resolved params
19
+ * @returns {string}
20
+ *
21
+ * @example
22
+ * captionId({ ...params, tableId: 'users' }); // → 'users-caption'
23
+ * captionId(params); // → 'jt-1-caption'
24
+ */
25
+ export function captionId(params) {
26
+ return params.tableId ? `${params.tableId}-caption` : `jt-${++captionUid}-caption`;
27
+ }
28
+
29
+
30
+ /**
31
+ * domBuilder item of the caption (template slot `caption`), rendered only when `params.caption`
32
+ * is set. It is a plain container placed below the table (footer bar) instead of a `<caption>`
33
+ * element: the table is linked to it through `aria-labelledby`, which gives the table the same
34
+ * accessible name a `<caption>` would.
35
+ *
36
+ * Generated structure: `div.caption[classes.caption]#<id>`; the element is stored in `elements.caption`.
37
+ *
38
+ * @param {JsonTable} jt - The component instance
39
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
40
+ * @param {string} id - Id of the element (see `captionId`)
41
+ * @returns {DomBuilderItem}
42
+ *
43
+ * @example
44
+ * domBuilder([captionPart(jt, elements, 'users-caption')], host);
45
+ * elements.caption; // → HTMLElement (when params.caption is set)
46
+ */
47
+ export function captionPart(jt, elements, id) {
48
+
49
+ const { params } = jt;
50
+
51
+ return {
52
+ id,
53
+ condition: params.caption != null,
54
+ className: classnames(styles.caption, params.classes.caption),
55
+ content: params.caption,
56
+ callback: el => { elements.caption = /** @type {HTMLElement} */ (el); }
57
+ };
58
+ }
59
+
60
+
61
+ /**
62
+ * domBuilder item of the footer bar below the table: caption (start side) and pagination (end
63
+ * side). Rendered only when `children` is not empty (see `mainBuilder`, which removes the parts
64
+ * placed elsewhere by the template).
65
+ *
66
+ * Generated structure: `div.tableFooter[classes.tableFooter] > [caption] [pagination]`; the element
67
+ * is stored in `elements.tableFooter`.
68
+ *
69
+ * @param {JsonTable} jt - The component instance
70
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
71
+ * @param {DomBuilderItem[]} children - Footer parts (caption and/or pagination items)
72
+ * @returns {DomBuilderItem}
73
+ *
74
+ * @example
75
+ * const footer = tableFooterPart(jt, elements, [captionItem, paginationItem]);
76
+ * domBuilder([tablePart(jt, elements, footer)], host);
77
+ */
78
+ export function tableFooterPart(jt, elements, children) {
79
+
80
+ const { params } = jt;
81
+
82
+ return {
83
+ condition: children.length > 0,
84
+ className: classnames(styles.tableFooter, params.classes.tableFooter),
85
+ children,
86
+ callback: el => { elements.tableFooter = /** @type {HTMLElement} */ (el); }
87
+ };
88
+ }
89
+
90
+
91
+ /**
92
+ * domBuilder item of the table (template slot `table`).
93
+ *
94
+ * Generated structure:
95
+ * ```
96
+ * div.tableWrapper[classes.tableWrapper]
97
+ * table#tableId.table[classes.table][aria-labelledby=<caption id>] (aria-labelledby if `caption`)
98
+ * thead ← theadPart()
99
+ * tbody ← filled by `renderTbody()`
100
+ * tfoot (if `tfoot`) ← filled by `renderTfoot()`
101
+ * div.tableFooter ← footerBar (caption + pagination), if given
102
+ * ```
103
+ *
104
+ * References to the generated elements are stored in `elements`
105
+ * (`tableWrapper`, `table`, `thead`, `tbody`, `tfoot`).
106
+ *
107
+ * @param {JsonTable} jt - The component instance (`params`, `cols`)
108
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
109
+ * @param {DomBuilderItem|null} [footerBar=null] - Footer bar item (see `tableFooterPart`) (default: null)
110
+ * @param {string|null} [captionElId=null] - Id of the caption element, for `aria-labelledby` (default: null)
111
+ * @returns {DomBuilderItem} A `div` wrapping the table wrapper and the footer bar
112
+ *
113
+ * @example
114
+ * domBuilder([tablePart(jt, elements, footerBar, 'users-caption')], host);
115
+ * elements.tbody; // → HTMLTableSectionElement (empty until renderTbody() is called)
116
+ */
117
+ export function tablePart(jt, elements, footerBar = null, captionElId = null) {
118
+
119
+ const { params } = jt;
120
+
121
+ return {
122
+ className: styles.tableOuter,
123
+ children: [
124
+ {
125
+ className: classnames(styles.tableWrapper, params.classes.tableWrapper),
126
+ callback: el => { elements.tableWrapper = /** @type {HTMLElement} */ (el); },
127
+ children: [
128
+ {
129
+ tag: 'table',
130
+ id: params.tableId,
131
+ className: classnames(styles.table, params.classes.table),
132
+ attrs: { 'aria-labelledby': params.caption != null ? captionElId : null },
133
+ callback: el => { elements.table = /** @type {HTMLTableElement} */ (el); },
134
+ children: [
135
+ theadPart(jt, elements),
136
+ {
137
+ tag: 'tbody',
138
+ callback: el => { elements.tbody = /** @type {HTMLTableSectionElement} */ (el); }
139
+ },
140
+ {
141
+ tag: 'tfoot',
142
+ condition: params.tfoot,
143
+ callback: el => { elements.tfoot = /** @type {HTMLTableSectionElement} */ (el); }
144
+ }
145
+ ]
146
+ }
147
+ ]
148
+ },
149
+ ...(footerBar ? [footerBar] : [])
150
+ ]
151
+ };
152
+ }