@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.
- package/README.md +2 -1
- package/dev-tools/starter-kit/source_files/_private/pwd.md +16 -0
- package/dev-tools/starter-kit/source_files/_private/sf.md +12 -0
- package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +2 -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/{package-tpl.json → source_files/root/package-tpl.json} +1 -1
- package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +1 -1
- 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} +56 -38
- package/dev-tools/starter-kit/starter-install.sh +233 -81
- package/dev-tools/stylelint-config/README.md +45 -0
- package/dev-tools/stylelint-config/index.js +184 -0
- package/dev-tools/stylelint-config/test/test.module.scss +9 -0
- package/package.json +11 -2
- package/snippets/php/iviewer.php +488 -0
- package/snippets/twig-symfony/ViewerController.php +549 -0
- package/src/custom-properties.css +36 -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 +632 -0
- package/src/web-components/json-table/json-table-component.module.css +247 -0
- package/src/web-components/json-table/json-table.minimo.tokens.mjs +243 -0
- package/src/web-components/json-table/readme.md +764 -0
- 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 +424 -0
- package/src/web-components/json-table/src/get-data.js +152 -0
- package/src/web-components/json-table/src/icons.js +80 -0
- package/src/web-components/json-table/src/info-section.js +107 -0
- package/src/web-components/json-table/src/main-builder.js +218 -0
- 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 +166 -0
- 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 +67 -0
- package/src/web-components/s-datatable-component/{docs.md → readme.md} +67 -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/{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,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
|
+
}
|