@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,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
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/*! minimo - json-table: tfoot rendering */
|
|
2
|
+
|
|
3
|
+
import { domBuilder } from '../../../utilities/dom-builder/dom-builder.js';
|
|
4
|
+
import { classnames } from '../../../utilities/classnames.js';
|
|
5
|
+
import { setContent } from './content-utils.js';
|
|
6
|
+
import { tfootContent } from './cell-content.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Renders the footer row inside `elements.tfoot` (present only when `params.tfoot` is true),
|
|
12
|
+
* replacing the previous content. Every column produces a cell whose content comes from
|
|
13
|
+
* `tfootContent()` (column `tfootRender`); the footer is emptied when there are no rows.
|
|
14
|
+
*
|
|
15
|
+
* The rows passed to `tfootRender` are the whole filtered set, or the current page only when
|
|
16
|
+
* `updateFooterOnPageChange` is true.
|
|
17
|
+
*
|
|
18
|
+
* Generated structure: `tr > td[data-key][cellClass]…`
|
|
19
|
+
*
|
|
20
|
+
* @param {JsonTable} jt - The component instance
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* renderTfoot(jt); // after `jt.state` has been updated
|
|
25
|
+
*/
|
|
26
|
+
export function renderTfoot(jt) {
|
|
27
|
+
|
|
28
|
+
const { params, elements, state, cols } = jt;
|
|
29
|
+
const tfoot = elements.tfoot;
|
|
30
|
+
|
|
31
|
+
if (!tfoot) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!state.filtered.length) {
|
|
36
|
+
tfoot.replaceChildren();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const rows = (params.updateFooterOnPageChange ? state.pageRows : state.filtered).map(parsed => parsed.row);
|
|
41
|
+
|
|
42
|
+
domBuilder([
|
|
43
|
+
{
|
|
44
|
+
tag: 'tr',
|
|
45
|
+
children: cols.map(col => ({
|
|
46
|
+
tag: 'td',
|
|
47
|
+
className: classnames(col.cellClass),
|
|
48
|
+
attrs: { 'data-key': col.key },
|
|
49
|
+
callback: el => {
|
|
50
|
+
const td = /** @type {HTMLTableCellElement} */ (el);
|
|
51
|
+
setContent(td, tfootContent(col, rows, params, td));
|
|
52
|
+
}
|
|
53
|
+
}))
|
|
54
|
+
}
|
|
55
|
+
], tfoot, { emptyParent: true });
|
|
56
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/*! minimo - json-table: thead */
|
|
2
|
+
|
|
3
|
+
import * as styles from '../json-table-component.module.css';
|
|
4
|
+
import { classnames } from '../../../utilities/classnames.js';
|
|
5
|
+
import { setContent } from './content-utils.js';
|
|
6
|
+
import { iconContent } from './icons.js';
|
|
7
|
+
import { nextSortDir } from './sorting.js';
|
|
8
|
+
|
|
9
|
+
/** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
|
|
10
|
+
/** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
|
|
11
|
+
/** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
|
|
12
|
+
|
|
13
|
+
/** @typedef {'none'|'ascending'|'descending'} SortState */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Updates the sort state of a sortable `<th>`: `aria-sort` attribute (removed when `none`),
|
|
17
|
+
* icon and `title`/`aria-label` of the sort button (both describing the action of the next
|
|
18
|
+
* click, e.g. "sort descending" when the ascending sort is active).
|
|
19
|
+
*
|
|
20
|
+
* @param {HTMLTableCellElement} th - The `<th>` generated by `theadPart()`
|
|
21
|
+
* @param {SortState} state - Sort state to apply
|
|
22
|
+
* @param {JsonTableParams} params - Resolved params (icons and labels)
|
|
23
|
+
* @returns {void}
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* setSortState(th, 'ascending', params);
|
|
27
|
+
* // th[aria-sort=ascending], icon → params.sortAscArrowIcon, button title → params.labels.sortDesc
|
|
28
|
+
*/
|
|
29
|
+
export function setSortState(th, state, params) {
|
|
30
|
+
|
|
31
|
+
const btn = th.querySelector('button');
|
|
32
|
+
const icon = th.querySelector('[data-jt-sort-icon]');
|
|
33
|
+
|
|
34
|
+
if (state === 'none') {
|
|
35
|
+
th.removeAttribute('aria-sort');
|
|
36
|
+
} else {
|
|
37
|
+
th.setAttribute('aria-sort', state);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const nextLabel = state === 'none'
|
|
41
|
+
? params.labels.sortAsc
|
|
42
|
+
: (state === 'ascending' ? params.labels.sortDesc : params.labels.sortNone);
|
|
43
|
+
|
|
44
|
+
if (btn) {
|
|
45
|
+
const titleText = (th.querySelector('[data-jt-sort-title]')?.textContent ?? '').trim();
|
|
46
|
+
btn.title = nextLabel ?? '';
|
|
47
|
+
btn.setAttribute('aria-label', titleText ? `${titleText}: ${nextLabel}` : (nextLabel ?? ''));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (icon) {
|
|
51
|
+
const iconDef = state === 'none'
|
|
52
|
+
? params.sortNoneArrowIcon
|
|
53
|
+
: (state === 'ascending' ? params.sortAscArrowIcon : params.sortDescArrowIcon);
|
|
54
|
+
setContent(/** @type {HTMLElement} */ (icon), iconContent(iconDef));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Applies the current sort (`jt.state.sort`) to every sortable `<th>` of the thead: the sorted
|
|
61
|
+
* column gets `aria-sort` and the matching icon, the others are reset to `none`.
|
|
62
|
+
*
|
|
63
|
+
* @param {JsonTable} jt - The component instance
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* jt.state.sort = { key: 'name', dir: 'desc' };
|
|
68
|
+
* applySortState(jt); // th[data-key=name][aria-sort=descending], the other th without aria-sort
|
|
69
|
+
*/
|
|
70
|
+
export function applySortState(jt) {
|
|
71
|
+
|
|
72
|
+
const { elements, params, state } = jt;
|
|
73
|
+
|
|
74
|
+
if (!elements.thead) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
elements.thead.querySelectorAll('th[data-sortable]').forEach(el => {
|
|
79
|
+
const th = /** @type {HTMLTableCellElement} */ (el);
|
|
80
|
+
const isSorted = state.sort != null && th.dataset.key === state.sort.key;
|
|
81
|
+
setSortState(th, isSorted ? (state.sort?.dir === 'desc' ? 'descending' : 'ascending') : 'none', params);
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Attaches the delegated `click` listener to the thead: a click on a sort button cycles the
|
|
88
|
+
* column sort (none → asc → desc → none, see `nextSortDir`) through `jt.setSort()`.
|
|
89
|
+
* The thead is not rebuilt on sort, so the focus stays on the clicked button.
|
|
90
|
+
*
|
|
91
|
+
* @param {JsonTable} jt - The component instance
|
|
92
|
+
* @returns {void}
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* setSortListener(jt); // after mainBuilder(jt)
|
|
96
|
+
*/
|
|
97
|
+
export function setSortListener(jt) {
|
|
98
|
+
|
|
99
|
+
const thead = jt.elements.thead;
|
|
100
|
+
|
|
101
|
+
if (!thead) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
thead.addEventListener('click', e => {
|
|
106
|
+
const btn = /** @type {HTMLElement|null} */ (
|
|
107
|
+
/** @type {HTMLElement} */ (e.target).closest('button[data-key]')
|
|
108
|
+
);
|
|
109
|
+
if (!btn) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const key = btn.dataset.key ?? '';
|
|
113
|
+
const current = jt.state.sort?.key === key ? jt.state.sort.dir : null;
|
|
114
|
+
jt.setSort(key, nextSortDir(current));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* domBuilder item of the `<thead>`.
|
|
121
|
+
*
|
|
122
|
+
* Generated structure:
|
|
123
|
+
* ```
|
|
124
|
+
* thead > tr
|
|
125
|
+
* th[scope=col][data-key][headerClass] ← not sortable: content = col.title
|
|
126
|
+
* th.sortable[scope=col][data-key][data-sortable=true][headerClass] ← sortable
|
|
127
|
+
* button.sortBtn[classes.sortBtn][type=button][data-key][title][aria-label]
|
|
128
|
+
* span.sortTitle[data-jt-sort-title] ← col.title
|
|
129
|
+
* span.sortIcon[data-jt-sort-icon] ← sort icon (see `setSortState`)
|
|
130
|
+
* ```
|
|
131
|
+
*
|
|
132
|
+
* The `<thead>` is stored in `elements.thead`; the sort listener is attached by `setSortListener()`.
|
|
133
|
+
*
|
|
134
|
+
* @param {JsonTable} jt - The component instance (`params`, `cols`)
|
|
135
|
+
* @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
|
|
136
|
+
* @returns {DomBuilderItem}
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* domBuilder([{ tag: 'table', children: [theadPart(jt, elements)] }], host);
|
|
140
|
+
* elements.thead; // → HTMLTableSectionElement
|
|
141
|
+
*/
|
|
142
|
+
export function theadPart(jt, elements) {
|
|
143
|
+
|
|
144
|
+
const { params, cols } = jt;
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
tag: 'thead',
|
|
148
|
+
callback: el => { elements.thead = /** @type {HTMLTableSectionElement} */ (el); },
|
|
149
|
+
children: [
|
|
150
|
+
{
|
|
151
|
+
tag: 'tr',
|
|
152
|
+
children: cols.map(col => ({
|
|
153
|
+
tag: 'th',
|
|
154
|
+
className: classnames(col.headerClass, col.sortable && styles.sortable),
|
|
155
|
+
attrs: {
|
|
156
|
+
scope: 'col',
|
|
157
|
+
'data-key': col.key,
|
|
158
|
+
'data-sortable': col.sortable ? 'true' : null
|
|
159
|
+
},
|
|
160
|
+
content: col.sortable ? null : col.title,
|
|
161
|
+
children: col.sortable
|
|
162
|
+
? [
|
|
163
|
+
{
|
|
164
|
+
tag: 'button',
|
|
165
|
+
className: classnames(styles.sortBtn, params.classes.sortBtn),
|
|
166
|
+
attrs: { type: 'button', 'data-key': col.key },
|
|
167
|
+
children: [
|
|
168
|
+
{ tag: 'span', className: styles.sortTitle, attrs: { 'data-jt-sort-title': '' }, content: col.title },
|
|
169
|
+
{ tag: 'span', className: styles.sortIcon, attrs: { 'data-jt-sort-icon': '', 'aria-hidden': 'true' } }
|
|
170
|
+
]
|
|
171
|
+
}
|
|
172
|
+
]
|
|
173
|
+
: [],
|
|
174
|
+
callback: el => {
|
|
175
|
+
if (col.sortable) {
|
|
176
|
+
setSortState(/** @type {HTMLTableCellElement} */ (el), 'none', params);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}))
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
};
|
|
183
|
+
}
|