@massimo-cassandro/minimo 1.33.0 → 1.33.2
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 +163 -82
- 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,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
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,22 +1,93 @@
|
|
|
1
1
|
/*! minimo - json-table: params resolution */
|
|
2
2
|
|
|
3
|
-
import { defaults } from './defaults.js';
|
|
3
|
+
import { defaults, mergedParams } from './defaults.js';
|
|
4
4
|
|
|
5
5
|
/** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
|
|
6
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
|
+
|
|
7
78
|
/**
|
|
8
79
|
* Resolves a single parameter, merging the four possible sources.
|
|
9
80
|
*
|
|
10
81
|
* Precedence (highest first):
|
|
11
82
|
* 1. `config` – value passed via `init()`/`reload()`. Present and `!== undefined` always wins;
|
|
12
83
|
* pass `null` explicitly to skip the HTML attribute and fall back to the defaults.
|
|
13
|
-
* 2. HTML attribute
|
|
14
|
-
* (`search`, `search="true"`, `search="1"` → true; `search="false"`, `search="0"` → false);
|
|
15
|
-
* values starting with `[` or `{` are parsed as JSON (invalid JSON is reported in the
|
|
16
|
-
* console and ignored, falling back to the defaults).
|
|
84
|
+
* 2. HTML attribute (see `readAttr`).
|
|
17
85
|
* 3. `projectDefaults` – values set via `JsonTable.setDefaults()`.
|
|
18
86
|
* 4. Built-in default (see `defaults.js`).
|
|
19
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
|
+
*
|
|
20
91
|
* @param {HTMLElement} el - The `<json-table>` element (source of the HTML attributes)
|
|
21
92
|
* @param {Partial<JsonTableParams>|null} config - Config passed via `init()` (default: null)
|
|
22
93
|
* @param {Partial<JsonTableParams>} projectDefaults - Project-wide defaults (default: {})
|
|
@@ -24,41 +95,40 @@ import { defaults } from './defaults.js';
|
|
|
24
95
|
* @returns {*} The resolved value
|
|
25
96
|
*
|
|
26
97
|
* @example
|
|
27
|
-
* // <json-table jsonurl="/api/rows.json" search="false"></json-table>
|
|
98
|
+
* // <json-table jsonurl="/api/rows.json" search="false" classes='{"table":"table"}'></json-table>
|
|
28
99
|
* getParam(el, { jsonUrl: '/api/other.json' }, {}, 'jsonUrl'); // → '/api/other.json' (config)
|
|
29
100
|
* getParam(el, null, {}, 'search'); // → false (attribute)
|
|
30
|
-
* getParam(el, null, {
|
|
31
|
-
* getParam(el, null, {}, '
|
|
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)
|
|
32
105
|
*/
|
|
33
106
|
export function getParam(el, config = null, projectDefaults = {}, name) {
|
|
34
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
|
+
|
|
35
123
|
// 1. config from script (undefined = key not present → go down one level)
|
|
36
|
-
if (
|
|
37
|
-
return
|
|
124
|
+
if (configValue !== undefined) {
|
|
125
|
+
return configValue;
|
|
38
126
|
}
|
|
39
127
|
|
|
40
128
|
// 2. HTML attribute
|
|
41
|
-
const
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
if (typeof defaults[name] === 'boolean') {
|
|
46
|
-
const lower = trimmed.toLowerCase();
|
|
47
|
-
// empty string = attribute present without value → true (standard HTML)
|
|
48
|
-
return lower !== 'false' && lower !== '0';
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
52
|
-
try {
|
|
53
|
-
return JSON.parse(trimmed);
|
|
54
|
-
} catch (err) {
|
|
55
|
-
// eslint-disable-next-line no-console
|
|
56
|
-
console.error(`[json-table] attributo \`${name}\`: JSON non valido, valore ignorato (viene usato il default)`, err);
|
|
57
|
-
// fall through to the defaults
|
|
58
|
-
}
|
|
59
|
-
} else {
|
|
60
|
-
return attr;
|
|
61
|
-
}
|
|
129
|
+
const attrValue = readAttr(el, name);
|
|
130
|
+
if (attrValue !== undefined) {
|
|
131
|
+
return attrValue;
|
|
62
132
|
}
|
|
63
133
|
|
|
64
134
|
// 3. project defaults
|
|
@@ -81,7 +151,7 @@ export function getParam(el, config = null, projectDefaults = {}, name) {
|
|
|
81
151
|
*
|
|
82
152
|
* @example
|
|
83
153
|
* const params = resolveParams(el, { jsonUrl: '/api/rows.json', debug: true }, {});
|
|
84
|
-
* // → { debug: true, jsonUrl: '/api/rows.json', jsonDataField: 'data', data: null, ... }
|
|
154
|
+
* // → { debug: true, jsonUrl: '/api/rows.json', jsonDataField: 'data', data: null, cols: [], ... }
|
|
85
155
|
*/
|
|
86
156
|
export function resolveParams(el, config = null, projectDefaults = {}) {
|
|
87
157
|
|
|
@@ -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
|
+
}
|