@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,107 @@
|
|
|
1
|
+
/*! minimo - json-table: info section parts */
|
|
2
|
+
|
|
3
|
+
import * as styles from '../json-table-component.module.css';
|
|
4
|
+
import { classnames } from '../../../utilities/classnames.js';
|
|
5
|
+
|
|
6
|
+
/** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
|
|
7
|
+
/** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* domBuilder item of the info text container (template slot `resultInfo`).
|
|
11
|
+
*
|
|
12
|
+
* Generated structure: `div.resultInfo[classes.resultInfo][aria-live=polite]`; the content is
|
|
13
|
+
* set by `update-info.js`. The element is stored in `elements.resultInfo`.
|
|
14
|
+
*
|
|
15
|
+
* @param {JsonTableParams} params - Resolved params
|
|
16
|
+
* @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
|
|
17
|
+
* @returns {DomBuilderItem}
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* domBuilder([resultInfoPart(params, elements)], host);
|
|
21
|
+
* elements.resultInfo; // → HTMLElement
|
|
22
|
+
*/
|
|
23
|
+
export function resultInfoPart(params, elements) {
|
|
24
|
+
return {
|
|
25
|
+
className: classnames(styles.resultInfo, params.classes.resultInfo),
|
|
26
|
+
attrs: { 'aria-live': 'polite' },
|
|
27
|
+
callback: el => { elements.resultInfo = /** @type {HTMLElement} */ (el); }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* domBuilder item of the search input (template slot `search`), rendered only when
|
|
34
|
+
* `params.search` is true.
|
|
35
|
+
*
|
|
36
|
+
* Generated structure: `div.search[classes.search] > input[type=search][classes.searchInput]`.
|
|
37
|
+
* The input is stored in `elements.searchInput`.
|
|
38
|
+
*
|
|
39
|
+
* TODO listener sull'input di ricerca: step 3
|
|
40
|
+
*
|
|
41
|
+
* @param {JsonTableParams} params - Resolved params
|
|
42
|
+
* @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
|
|
43
|
+
* @returns {DomBuilderItem}
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* domBuilder([searchPart(params, elements)], host);
|
|
47
|
+
* elements.searchInput; // → HTMLInputElement (when params.search is true)
|
|
48
|
+
*/
|
|
49
|
+
export function searchPart(params, elements) {
|
|
50
|
+
return {
|
|
51
|
+
condition: params.search,
|
|
52
|
+
className: classnames(styles.search, params.classes.search),
|
|
53
|
+
children: [
|
|
54
|
+
{
|
|
55
|
+
tag: 'input',
|
|
56
|
+
className: classnames(params.classes.searchInput),
|
|
57
|
+
attrs: {
|
|
58
|
+
type: 'search',
|
|
59
|
+
title: params.labels.searchTitle,
|
|
60
|
+
placeholder: params.labels.searchPlaceholder,
|
|
61
|
+
'aria-label': params.labels.searchAriaLabel
|
|
62
|
+
},
|
|
63
|
+
callback: el => { elements.searchInput = /** @type {HTMLInputElement} */ (el); }
|
|
64
|
+
}
|
|
65
|
+
]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* domBuilder item of the whole info section (template slot `infoSection`): info text + search input.
|
|
72
|
+
*
|
|
73
|
+
* Generated structure:
|
|
74
|
+
* ```
|
|
75
|
+
* div.infoOuter[classes.infoOuter]
|
|
76
|
+
* div.info[classes.info]
|
|
77
|
+
* div.resultInfo ← resultInfoPart()
|
|
78
|
+
* div.search > input[type=search] ← searchPart(), only if `params.search` is true
|
|
79
|
+
* ```
|
|
80
|
+
*
|
|
81
|
+
* References to the generated elements are stored in `elements`
|
|
82
|
+
* (`infoOuter`, `info`, `resultInfo`, `searchInput`).
|
|
83
|
+
*
|
|
84
|
+
* @param {JsonTableParams} params - Resolved params
|
|
85
|
+
* @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
|
|
86
|
+
* @returns {DomBuilderItem}
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* domBuilder([{ tag: 'section', children: [infoSectionPart(params, elements)] }], host);
|
|
90
|
+
* elements.info; // → HTMLElement
|
|
91
|
+
*/
|
|
92
|
+
export function infoSectionPart(params, elements) {
|
|
93
|
+
return {
|
|
94
|
+
className: classnames(styles.infoOuter, params.classes.infoOuter),
|
|
95
|
+
callback: el => { elements.infoOuter = /** @type {HTMLElement} */ (el); },
|
|
96
|
+
children: [
|
|
97
|
+
{
|
|
98
|
+
className: classnames(styles.info, params.classes.info),
|
|
99
|
+
callback: el => { elements.info = /** @type {HTMLElement} */ (el); },
|
|
100
|
+
children: [
|
|
101
|
+
resultInfoPart(params, elements),
|
|
102
|
+
searchPart(params, elements)
|
|
103
|
+
]
|
|
104
|
+
}
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/*! minimo - json-table: main structure builder */
|
|
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 { defaults } from './defaults.js';
|
|
7
|
+
import { infoSectionPart, resultInfoPart, searchPart } from './info-section.js';
|
|
8
|
+
import { tablePart, tableFooterPart, captionPart, captionId } from './table-builder.js';
|
|
9
|
+
import { paginationPart } from './pagination.js';
|
|
10
|
+
|
|
11
|
+
/** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
|
|
12
|
+
/** @typedef {import('./defaults.js').TemplateItem} TemplateItem */
|
|
13
|
+
/** @typedef {import('./defaults.js').TemplateSlot} TemplateSlot */
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Elements generated by the component, collected during the build.
|
|
17
|
+
* @typedef {Object} JsonTableElements
|
|
18
|
+
* @property {HTMLElement} [wrapper] - Main wrapper (`section`), contains the template parts
|
|
19
|
+
* @property {HTMLElement} [infoOuter] - Outer info container
|
|
20
|
+
* @property {HTMLElement} [info] - Info container (flex row: info text + search)
|
|
21
|
+
* @property {HTMLElement} [resultInfo] - Info text container
|
|
22
|
+
* @property {HTMLInputElement} [searchInput] - Search input (only when `search` is true)
|
|
23
|
+
* @property {HTMLElement} [tableWrapper] - Div wrapping the table (`table-responsive` by default)
|
|
24
|
+
* @property {HTMLTableElement} [table] - The `<table>` element
|
|
25
|
+
* @property {HTMLTableSectionElement} [thead] - The `<thead>` element
|
|
26
|
+
* @property {HTMLTableSectionElement} [tbody] - The `<tbody>` element
|
|
27
|
+
* @property {HTMLTableSectionElement} [tfoot] - The `<tfoot>` element (only when `tfoot` is true)
|
|
28
|
+
* @property {HTMLElement} [tableFooter] - Footer bar below the table (caption + pagination), when rendered
|
|
29
|
+
* @property {HTMLElement} [caption] - Caption container (only when `caption` is set)
|
|
30
|
+
* @property {HTMLElement} [pagination] - Pagination `<nav>` (only when `perPage` > 0)
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether a template item is a slot placeholder.
|
|
35
|
+
* @param {*} item
|
|
36
|
+
* @returns {item is { slot: TemplateSlot }}
|
|
37
|
+
*/
|
|
38
|
+
function isSlot(item) {
|
|
39
|
+
return item != null && typeof item === 'object' && !(item instanceof Node) && typeof item.slot === 'string';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Collects, recursively (`children` and array `content`), the names of the slots used by a template.
|
|
45
|
+
*
|
|
46
|
+
* @param {Array<TemplateItem|string|Node>} items - Template items
|
|
47
|
+
* @param {Set<string>} [found=new Set()] - Accumulator (default: new Set())
|
|
48
|
+
* @returns {Set<string>}
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* collectSlots([{ slot: 'table' }, { children: [{ slot: 'pagination' }] }]); // → Set { 'table', 'pagination' }
|
|
52
|
+
*/
|
|
53
|
+
export function collectSlots(items, found = new Set()) {
|
|
54
|
+
|
|
55
|
+
items.forEach(item => {
|
|
56
|
+
if (isSlot(item)) {
|
|
57
|
+
found.add(item.slot);
|
|
58
|
+
} else if (item != null && typeof item === 'object' && !(item instanceof Node)) {
|
|
59
|
+
if (Array.isArray(item.children)) {
|
|
60
|
+
collectSlots(item.children, found);
|
|
61
|
+
}
|
|
62
|
+
if (Array.isArray(item.content)) {
|
|
63
|
+
collectSlots(/** @type {TemplateItem[]} */ (item.content), found);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Replaces, recursively (`children` and array `content`), every `{ slot: 'name' }` item of a
|
|
74
|
+
* template with the corresponding built-in part. Unknown slots are reported in the console
|
|
75
|
+
* and skipped. Non-slot items are shallow-copied, so the original template is never mutated.
|
|
76
|
+
*
|
|
77
|
+
* @param {Array<TemplateItem|string|Node>} items - Template items
|
|
78
|
+
* @param {Object<string, DomBuilderItem>} parts - Built-in parts, keyed by slot name
|
|
79
|
+
* @returns {Array<DomBuilderItem|string|Node>}
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* expandSlots([{ className: 'row', children: [{ slot: 'search' }, { slot: 'resultInfo' }] }, { slot: 'table' }], parts);
|
|
83
|
+
* // → [{ className: 'row', children: [<search part>, <resultInfo part>] }, <table part>]
|
|
84
|
+
*/
|
|
85
|
+
export function expandSlots(items, parts) {
|
|
86
|
+
|
|
87
|
+
/** @type {Array<DomBuilderItem|string|Node>} */
|
|
88
|
+
const result = [];
|
|
89
|
+
|
|
90
|
+
items.forEach(item => {
|
|
91
|
+
|
|
92
|
+
if (item == null || typeof item !== 'object' || item instanceof Node) {
|
|
93
|
+
result.push(/** @type {string|Node} */ (item));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isSlot(item)) {
|
|
98
|
+
const part = parts[item.slot];
|
|
99
|
+
if (!part) {
|
|
100
|
+
// eslint-disable-next-line no-console
|
|
101
|
+
console.error(`[json-table] template: slot \`${item.slot}\` non riconosciuto (disponibili: ${Object.keys(parts).join(', ')})`);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
result.push(part);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** @type {DomBuilderItem} */
|
|
109
|
+
const copy = /** @type {DomBuilderItem} */ ({ ...item });
|
|
110
|
+
|
|
111
|
+
if (Array.isArray(item.children)) {
|
|
112
|
+
copy.children = expandSlots(item.children, parts);
|
|
113
|
+
}
|
|
114
|
+
if (Array.isArray(item.content)) {
|
|
115
|
+
copy.content = /** @type {DomBuilderItem[]} */ (expandSlots(/** @type {TemplateItem[]} */ (item.content), parts));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
result.push(copy);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds the whole component structure inside the host element (emptied first) via domBuilder:
|
|
127
|
+
* a fixed main wrapper whose content is defined by `params.template`.
|
|
128
|
+
*
|
|
129
|
+
* Available template slots (see `TemplateItem`):
|
|
130
|
+
* - `infoSection` → `infoSectionPart()` (info text + search input)
|
|
131
|
+
* - `resultInfo` → `resultInfoPart()` (info text only)
|
|
132
|
+
* - `search` → `searchPart()` (search input only)
|
|
133
|
+
* - `table` → `tablePart()` (table wrapper + table with thead, tbody, tfoot, plus the footer bar)
|
|
134
|
+
* - `caption` → `captionPart()` (caption container; part of the `table` footer bar unless placed explicitly)
|
|
135
|
+
* - `pagination` → `paginationPart()` (pagination nav; idem)
|
|
136
|
+
*
|
|
137
|
+
* Generated structure with the default template (`[{ slot: 'infoSection' }, { slot: 'table' }]`):
|
|
138
|
+
* ```
|
|
139
|
+
* section.wrapper[classes.wrapper]
|
|
140
|
+
* div.infoOuter[classes.infoOuter]
|
|
141
|
+
* div.info[classes.info]
|
|
142
|
+
* div.resultInfo
|
|
143
|
+
* div.search > input[type=search] (if `search`)
|
|
144
|
+
* div.tableOuter
|
|
145
|
+
* div.tableWrapper[classes.tableWrapper]
|
|
146
|
+
* table#tableId.table[classes.table]
|
|
147
|
+
* thead / tbody / tfoot (tfoot if `tfoot`)
|
|
148
|
+
* div.tableFooter[classes.tableFooter] (if `caption` or `perPage` > 0)
|
|
149
|
+
* div.caption (if `caption`)
|
|
150
|
+
* nav.pagination (if `perPage` > 0)
|
|
151
|
+
* ```
|
|
152
|
+
*
|
|
153
|
+
* The `<tbody>`, `<tfoot>` and pagination are created empty: see `renderTbody()`, `renderTfoot()`
|
|
154
|
+
* and `renderPagination()`.
|
|
155
|
+
*
|
|
156
|
+
* @param {JsonTable} jt - The component instance (host element, `params`, `cols`)
|
|
157
|
+
* @returns {JsonTableElements} References to the generated elements
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* jt.params.template = [
|
|
161
|
+
* { className: 'flex gap-2', children: [{ slot: 'search' }, { slot: 'pagination' }] },
|
|
162
|
+
* { slot: 'table' }, // footer bar with the caption only: the pagination is placed above
|
|
163
|
+
* { slot: 'resultInfo' }
|
|
164
|
+
* ];
|
|
165
|
+
* const elements = mainBuilder(jt);
|
|
166
|
+
* elements.table; // → HTMLTableElement
|
|
167
|
+
*/
|
|
168
|
+
export function mainBuilder(jt) {
|
|
169
|
+
|
|
170
|
+
const { params } = jt;
|
|
171
|
+
|
|
172
|
+
/** @type {JsonTableElements} */
|
|
173
|
+
const elements = {};
|
|
174
|
+
|
|
175
|
+
const capId = captionId(params);
|
|
176
|
+
const captionItem = captionPart(jt, elements, capId);
|
|
177
|
+
const paginationItem = paginationPart(jt, elements);
|
|
178
|
+
|
|
179
|
+
// footer bar parts: removed below when the template places them elsewhere
|
|
180
|
+
/** @type {DomBuilderItem[]} */
|
|
181
|
+
const footerChildren = [captionItem, paginationItem];
|
|
182
|
+
const footerBar = tableFooterPart(jt, elements, footerChildren);
|
|
183
|
+
|
|
184
|
+
/** @type {Object<string, DomBuilderItem>} */
|
|
185
|
+
const parts = {
|
|
186
|
+
infoSection: infoSectionPart(params, elements),
|
|
187
|
+
resultInfo: resultInfoPart(params, elements),
|
|
188
|
+
search: searchPart(params, elements),
|
|
189
|
+
caption: captionItem,
|
|
190
|
+
pagination: paginationItem,
|
|
191
|
+
table: tablePart(jt, elements, footerBar, capId)
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const template = typeof params.template === 'function'
|
|
195
|
+
? params.template(parts, params)
|
|
196
|
+
: params.template;
|
|
197
|
+
|
|
198
|
+
const items = Array.isArray(template) ? template : /** @type {TemplateItem[]} */ (defaults.template);
|
|
199
|
+
const usedSlots = collectSlots(items);
|
|
200
|
+
|
|
201
|
+
['caption', 'pagination'].forEach(slot => {
|
|
202
|
+
if (usedSlots.has(slot)) {
|
|
203
|
+
footerChildren.splice(footerChildren.indexOf(parts[slot]), 1);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
footerBar.condition = footerChildren.some(item => item.condition !== false);
|
|
207
|
+
|
|
208
|
+
domBuilder([
|
|
209
|
+
{
|
|
210
|
+
tag: 'section',
|
|
211
|
+
className: classnames(styles.wrapper, params.classes.wrapper),
|
|
212
|
+
callback: el => { elements.wrapper = /** @type {HTMLElement} */ (el); },
|
|
213
|
+
children: expandSlots(items, parts)
|
|
214
|
+
}
|
|
215
|
+
], jt, { emptyParent: true });
|
|
216
|
+
|
|
217
|
+
return elements;
|
|
218
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/*! minimo - json-table: pagination */
|
|
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 { iconContent } from './icons.js';
|
|
7
|
+
|
|
8
|
+
/** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
|
|
9
|
+
/** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Number of pages needed to show `count` rows, `perPage` per page (at least 1).
|
|
13
|
+
* `perPage` <= 0 means no pagination: a single page.
|
|
14
|
+
*
|
|
15
|
+
* @param {number} count - Number of rows
|
|
16
|
+
* @param {number} perPage - Rows per page
|
|
17
|
+
* @returns {number}
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* calcTotPages(101, 25); // → 5
|
|
21
|
+
* calcTotPages(0, 25); // → 1
|
|
22
|
+
* calcTotPages(101, 0); // → 1
|
|
23
|
+
*/
|
|
24
|
+
export function calcTotPages(count, perPage) {
|
|
25
|
+
if (!(perPage > 0)) {
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
return Math.max(1, Math.ceil(count / perPage));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds the list of page numbers to show in the navigation: the current page with `delta`
|
|
34
|
+
* pages on each side (the window is widened near the edges so that it keeps a constant size),
|
|
35
|
+
* plus the first and the last page; `null` marks a gap (ellipsis). When the gap would hide a
|
|
36
|
+
* single page, that page is shown instead of the ellipsis.
|
|
37
|
+
*
|
|
38
|
+
* @param {number} current - Current page (1-based)
|
|
39
|
+
* @param {number} totPages - Total number of pages
|
|
40
|
+
* @param {number} [delta=2] - Pages on each side of the current one (default: 2)
|
|
41
|
+
* @returns {Array<number|null>}
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* pageList(1, 3); // → [1, 2, 3]
|
|
45
|
+
* pageList(1, 10); // → [1, 2, 3, 4, 5, null, 10]
|
|
46
|
+
* pageList(6, 12); // → [1, null, 4, 5, 6, 7, 8, null, 12]
|
|
47
|
+
* pageList(5, 10); // → [1, 2, 3, 4, 5, 6, 7, null, 10] (page 2 shown instead of an ellipsis hiding it alone)
|
|
48
|
+
* pageList(10, 10); // → [1, null, 6, 7, 8, 9, 10]
|
|
49
|
+
* pageList(4, 10, 1); // → [1, 2, 3, 4, 5, null, 10]
|
|
50
|
+
*/
|
|
51
|
+
export function pageList(current, totPages, delta = 2) {
|
|
52
|
+
|
|
53
|
+
const side = Math.max(0, Math.floor(delta));
|
|
54
|
+
const width = side * 2 + 1;
|
|
55
|
+
|
|
56
|
+
if (totPages <= width + 2) {
|
|
57
|
+
return Array.from({ length: totPages }, (_, i) => i + 1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let min = Math.max(1, current - side);
|
|
61
|
+
let max = Math.min(totPages, current + side);
|
|
62
|
+
|
|
63
|
+
// keep the window size constant near the edges
|
|
64
|
+
while (max - min + 1 < width) {
|
|
65
|
+
if (min > 1) {
|
|
66
|
+
min--;
|
|
67
|
+
} else if (max < totPages) {
|
|
68
|
+
max++;
|
|
69
|
+
} else {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** @type {Array<number|null>} */
|
|
75
|
+
const pages = Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
76
|
+
|
|
77
|
+
if (min === 3) {
|
|
78
|
+
pages.unshift(1, 2);
|
|
79
|
+
} else if (min > 3) {
|
|
80
|
+
pages.unshift(1, null);
|
|
81
|
+
} else if (min === 2) {
|
|
82
|
+
pages.unshift(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (max === totPages - 2) {
|
|
86
|
+
pages.push(totPages - 1, totPages);
|
|
87
|
+
} else if (max < totPages - 2) {
|
|
88
|
+
pages.push(null, totPages);
|
|
89
|
+
} else if (max === totPages - 1) {
|
|
90
|
+
pages.push(totPages);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return pages;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Replaces the `{page}` placeholder of a pagination label.
|
|
99
|
+
* @param {string|undefined} label
|
|
100
|
+
* @param {number} page
|
|
101
|
+
* @param {string} locale
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function pageLabel(label, page, locale) {
|
|
105
|
+
return String(label ?? '').replace(/\{page\}/g, page.toLocaleString(locale));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* domBuilder item of the pagination `<nav>` (template slot `pagination`), rendered only when
|
|
111
|
+
* `params.perPage` is > 0. The `<nav>` is stored in `elements.pagination` and gets a delegated
|
|
112
|
+
* `click` listener on its buttons, which calls `jt.goToPage()`. The content (page list) is
|
|
113
|
+
* rendered by `renderPagination()` and the whole `<nav>` is hidden when there is a single page.
|
|
114
|
+
*
|
|
115
|
+
* Generated structure (see `renderPagination`):
|
|
116
|
+
* ```
|
|
117
|
+
* nav.pagination[classes.pagination][aria-label=labels.paginationAriaLabel]
|
|
118
|
+
* ul.paginationList
|
|
119
|
+
* li.paginationItem > button.paginationBtn[classes.paginationBtn][data-page=prev][aria-label][title] > icon
|
|
120
|
+
* li.paginationItem > button.paginationBtn[data-page=1][aria-label="Vai a pagina 1"] 1
|
|
121
|
+
* li.paginationItem > button.paginationBtn[data-page=2][aria-current=page] 2 ← current page
|
|
122
|
+
* li.paginationItem > span.paginationEllipsis …
|
|
123
|
+
* li.paginationItem > button.paginationBtn[data-page=next] > icon
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @param {JsonTable} jt - The component instance
|
|
127
|
+
* @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
|
|
128
|
+
* @returns {DomBuilderItem}
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* domBuilder([paginationPart(jt, elements)], host);
|
|
132
|
+
* elements.pagination; // → HTMLElement (nav), when params.perPage > 0
|
|
133
|
+
*/
|
|
134
|
+
export function paginationPart(jt, elements) {
|
|
135
|
+
|
|
136
|
+
const { params } = jt;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
tag: 'nav',
|
|
140
|
+
condition: params.perPage > 0,
|
|
141
|
+
className: classnames(styles.pagination, params.classes.pagination),
|
|
142
|
+
attrs: { 'aria-label': params.labels.paginationAriaLabel, hidden: '' },
|
|
143
|
+
callback: el => {
|
|
144
|
+
const nav = /** @type {HTMLElement} */ (el);
|
|
145
|
+
elements.pagination = nav;
|
|
146
|
+
|
|
147
|
+
nav.addEventListener('click', e => {
|
|
148
|
+
const btn = /** @type {HTMLElement|null} */ (
|
|
149
|
+
/** @type {HTMLElement} */ (e.target).closest('button[data-page]')
|
|
150
|
+
);
|
|
151
|
+
if (!btn || btn.hasAttribute('disabled') || btn.getAttribute('aria-current') === 'page') {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const target = btn.dataset.page;
|
|
155
|
+
const { page } = jt.state;
|
|
156
|
+
const requested = target === 'prev' ? page - 1 : (target === 'next' ? page + 1 : Number(target));
|
|
157
|
+
jt.goToPage(requested, target);
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Renders the page list inside `elements.pagination` from the current state (`page`, `totPages`),
|
|
166
|
+
* replacing the previous content; the `<nav>` is hidden when there is a single page.
|
|
167
|
+
*
|
|
168
|
+
* When `focusTarget` is given (the `data-page` of the button that triggered the change:
|
|
169
|
+
* `'prev'`, `'next'` or a page number) the focus is moved to the corresponding new button, or
|
|
170
|
+
* to the current page button when that one is no longer available, so that keyboard users do
|
|
171
|
+
* not lose their position.
|
|
172
|
+
*
|
|
173
|
+
* @param {JsonTable} jt - The component instance
|
|
174
|
+
* @param {string|null} [focusTarget=null] - `data-page` of the button to focus after the rendering (default: null)
|
|
175
|
+
* @returns {void}
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* renderPagination(jt); // after `jt.state` has been updated
|
|
179
|
+
* renderPagination(jt, 'next'); // same, moving the focus to the "next" button
|
|
180
|
+
*/
|
|
181
|
+
export function renderPagination(jt, focusTarget = null) {
|
|
182
|
+
|
|
183
|
+
const { params, elements, state } = jt;
|
|
184
|
+
const nav = elements.pagination;
|
|
185
|
+
|
|
186
|
+
if (!nav) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (state.totPages <= 1) {
|
|
191
|
+
nav.hidden = true;
|
|
192
|
+
nav.replaceChildren();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
nav.hidden = false;
|
|
197
|
+
|
|
198
|
+
const btnClass = classnames(styles.paginationBtn, params.classes.paginationBtn);
|
|
199
|
+
const { labels, locale } = params;
|
|
200
|
+
|
|
201
|
+
/** @type {(target: string, label: string, icon: import('./defaults.js').IconDef, disabled: boolean) => DomBuilderItem} */
|
|
202
|
+
const arrowBtn = (target, label, icon, disabled) => ({
|
|
203
|
+
tag: 'li',
|
|
204
|
+
className: styles.paginationItem,
|
|
205
|
+
children: [
|
|
206
|
+
{
|
|
207
|
+
tag: 'button',
|
|
208
|
+
className: classnames(btnClass, styles.paginationArrow),
|
|
209
|
+
attrs: { type: 'button', 'data-page': target, 'aria-label': label, title: label, disabled: disabled ? '' : null },
|
|
210
|
+
content: iconContent(icon)
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
domBuilder([
|
|
216
|
+
{
|
|
217
|
+
tag: 'ul',
|
|
218
|
+
className: styles.paginationList,
|
|
219
|
+
children: [
|
|
220
|
+
arrowBtn('prev', labels.prevPage ?? '', params.paginationPrevIcon, state.page <= 1),
|
|
221
|
+
|
|
222
|
+
...pageList(state.page, state.totPages, params.paginationDelta).map(page => ({
|
|
223
|
+
tag: 'li',
|
|
224
|
+
className: styles.paginationItem,
|
|
225
|
+
children: [
|
|
226
|
+
page === null
|
|
227
|
+
? { tag: 'span', className: styles.paginationEllipsis, attrs: { 'aria-hidden': 'true' }, content: '…' }
|
|
228
|
+
: {
|
|
229
|
+
tag: 'button',
|
|
230
|
+
className: btnClass,
|
|
231
|
+
attrs: {
|
|
232
|
+
type: 'button',
|
|
233
|
+
'data-page': page,
|
|
234
|
+
'aria-current': page === state.page ? 'page' : null,
|
|
235
|
+
'aria-label': pageLabel(page === state.page ? labels.currentPage : labels.pageTitle, page, locale),
|
|
236
|
+
title: pageLabel(page === state.page ? labels.currentPage : labels.pageTitle, page, locale)
|
|
237
|
+
},
|
|
238
|
+
content: page.toLocaleString(locale)
|
|
239
|
+
}
|
|
240
|
+
]
|
|
241
|
+
})),
|
|
242
|
+
|
|
243
|
+
arrowBtn('next', labels.nextPage ?? '', params.paginationNextIcon, state.page >= state.totPages)
|
|
244
|
+
]
|
|
245
|
+
}
|
|
246
|
+
], nav, { emptyParent: true });
|
|
247
|
+
|
|
248
|
+
if (focusTarget != null) {
|
|
249
|
+
const wanted = /** @type {HTMLButtonElement|null} */ (nav.querySelector(`button[data-page="${focusTarget}"]:not([disabled])`));
|
|
250
|
+
const current = /** @type {HTMLButtonElement|null} */ (nav.querySelector('button[aria-current="page"]'));
|
|
251
|
+
(wanted ?? current)?.focus();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/*! minimo - json-table: columns parsing */
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
|
|
4
|
+
/** @typedef {import('./defaults.js').ColDefinition} ColDefinition */
|
|
5
|
+
/** @typedef {import('./defaults.js').DataTypeDefinition} DataTypeDefinition */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Column definition after parsing: every property has a value, `condition` is resolved to a
|
|
9
|
+
* boolean, `headerClass`/`cellClass` are the final class strings and `_type` is the data type object.
|
|
10
|
+
* @typedef {Required<Omit<ColDefinition, 'type'|'condition'|'sortValue'|'searchValue'>> & {
|
|
11
|
+
* condition: boolean,
|
|
12
|
+
* sortValue: *,
|
|
13
|
+
* searchValue: *,
|
|
14
|
+
* _type: DataTypeDefinition
|
|
15
|
+
* }} ParsedCol
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Column defaults.
|
|
20
|
+
* @type {Omit<Required<ColDefinition>, 'type'|'sortValue'|'searchValue'> & { sortValue: *, searchValue: * }}
|
|
21
|
+
*/
|
|
22
|
+
export const colDefaults = {
|
|
23
|
+
key: '',
|
|
24
|
+
title: null,
|
|
25
|
+
dataType: 'string',
|
|
26
|
+
render: null,
|
|
27
|
+
tfootRender: null,
|
|
28
|
+
rowHeading: false,
|
|
29
|
+
searchable: true,
|
|
30
|
+
sortable: true,
|
|
31
|
+
sortValue: undefined,
|
|
32
|
+
searchValue: undefined,
|
|
33
|
+
condition: true,
|
|
34
|
+
headerClass: null,
|
|
35
|
+
cellClass: null
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes the `cols` param: applies the column defaults and the data type `colDefaults`,
|
|
41
|
+
* resolves the `type`/`dataType` alias, the `condition` and the `headerClass`/`cellClass`
|
|
42
|
+
* fallbacks, and drops the columns whose `condition` is false.
|
|
43
|
+
*
|
|
44
|
+
* `cols` is required: an empty or missing definition throws.
|
|
45
|
+
*
|
|
46
|
+
* Merge order (lowest first): `colDefaults` ← data type `colDefaults` ← column definition.
|
|
47
|
+
* Class resolution: `headerClass` = column `headerClass` ?? column `cellClass` ?? type `headerClass`;
|
|
48
|
+
* `cellClass` = column `cellClass` ?? column `headerClass` ?? type `cellClass`.
|
|
49
|
+
*
|
|
50
|
+
* @param {ColDefinition[]|null|undefined} cols - Columns definition (`params.cols`)
|
|
51
|
+
* @param {Object<string, DataTypeDefinition>} dataTypes - Data types map (see `buildDataTypes`)
|
|
52
|
+
* @param {JsonTableParams} params - Resolved params (passed to `condition` functions)
|
|
53
|
+
* @returns {ParsedCol[]} Visible columns only
|
|
54
|
+
* @throws {Error} On missing/empty `cols`, missing `key` or unknown data type
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* parseCols([
|
|
58
|
+
* { key: 'id', dataType: 'id' }, // title: 'id' (default: key)
|
|
59
|
+
* { key: 'name', title: 'Nome', cellClass: 'fw-bold' }, // headerClass: 'fw-bold' (default: cellClass)
|
|
60
|
+
* { key: 'amount', type: 'euro', tfootRender: '@sum' }, // `type` alias of `dataType`
|
|
61
|
+
* { key: 'notes', condition: false } // dropped
|
|
62
|
+
* ], dataTypes, params);
|
|
63
|
+
*/
|
|
64
|
+
export function parseCols(cols, dataTypes, params) {
|
|
65
|
+
|
|
66
|
+
if (!Array.isArray(cols) || !cols.length) {
|
|
67
|
+
throw new Error('[json-table] parametro `cols` mancante o vuoto: la definizione delle colonne è obbligatoria');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return cols
|
|
71
|
+
.map((col, idx) => {
|
|
72
|
+
|
|
73
|
+
if (col == null || typeof col !== 'object') {
|
|
74
|
+
throw new Error(`[json-table] colonna #${idx}: definizione non valida`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const dataType = col.dataType ?? col.type ?? 'string';
|
|
78
|
+
const type = dataTypes[dataType];
|
|
79
|
+
|
|
80
|
+
if (!type) {
|
|
81
|
+
throw new Error(`[json-table] colonna #${idx} (${col.key}): dataType \`${dataType}\` non definito`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// `type` is only an alias of `dataType`: dropped from the merged definition
|
|
85
|
+
const colProps = { ...col };
|
|
86
|
+
delete colProps.type;
|
|
87
|
+
|
|
88
|
+
const merged = {
|
|
89
|
+
...colDefaults,
|
|
90
|
+
...(type.colDefaults ?? {}),
|
|
91
|
+
...colProps,
|
|
92
|
+
dataType
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
if (!merged.key || typeof merged.key !== 'string') {
|
|
96
|
+
throw new Error(`[json-table] colonna #${idx}: parametro \`key\` mancante`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const condition = typeof merged.condition === 'function'
|
|
100
|
+
? merged.condition(params)
|
|
101
|
+
: (merged.condition ?? true);
|
|
102
|
+
|
|
103
|
+
return /** @type {ParsedCol} */ ({
|
|
104
|
+
...merged,
|
|
105
|
+
title: merged.title ?? merged.key,
|
|
106
|
+
condition: Boolean(condition),
|
|
107
|
+
headerClass: merged.headerClass ?? merged.cellClass ?? type.headerClass ?? null,
|
|
108
|
+
cellClass: merged.cellClass ?? merged.headerClass ?? type.cellClass ?? null,
|
|
109
|
+
_type: type
|
|
110
|
+
});
|
|
111
|
+
})
|
|
112
|
+
.filter(col => col.condition);
|
|
113
|
+
}
|