@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
|
@@ -3,12 +3,50 @@
|
|
|
3
3
|
import { defaults } from './src/defaults.js';
|
|
4
4
|
import { resolveParams } from './src/resolve-params.js';
|
|
5
5
|
import { getData } from './src/get-data.js';
|
|
6
|
+
import { buildDataTypes } from './src/data-types.js';
|
|
7
|
+
import { parseCols } from './src/parse-cols.js';
|
|
8
|
+
import { parseRows } from './src/parse-rows.js';
|
|
6
9
|
import { mainBuilder } from './src/main-builder.js';
|
|
10
|
+
import { renderTbody } from './src/table-body.js';
|
|
11
|
+
import { renderTfoot } from './src/table-tfoot.js';
|
|
7
12
|
import { updateInfo } from './src/update-info.js';
|
|
13
|
+
import { applySortState, setSortListener } from './src/table-thead.js';
|
|
14
|
+
import { setSearchListener, filterRows } from './src/search.js';
|
|
15
|
+
import { sortRows } from './src/sorting.js';
|
|
16
|
+
import { calcTotPages, renderPagination } from './src/pagination.js';
|
|
17
|
+
import { domBuilder } from '../../utilities/dom-builder/dom-builder.js';
|
|
8
18
|
|
|
9
19
|
/** @typedef {import('./src/defaults.js').JsonTableParams} JsonTableParams */
|
|
20
|
+
/** @typedef {import('./src/defaults.js').DataTypeDefinition} DataTypeDefinition */
|
|
21
|
+
/** @typedef {import('./src/defaults.js').SortDef} SortDef */
|
|
22
|
+
/** @typedef {import('./src/parse-cols.js').ParsedCol} ParsedCol */
|
|
23
|
+
/** @typedef {import('./src/parse-rows.js').ParsedRow} ParsedRow */
|
|
10
24
|
/** @typedef {import('./src/main-builder.js').JsonTableElements} JsonTableElements */
|
|
11
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Rendering state.
|
|
28
|
+
*
|
|
29
|
+
* Client-side mode: `rows` holds every parsed row, `filtered` the rows after search and sort,
|
|
30
|
+
* `pageRows` the slice of the current page. Server-side mode: the three arrays hold the rows
|
|
31
|
+
* returned by the last request (the current page), `totRec`/`filteredRec` come from the JSON.
|
|
32
|
+
*
|
|
33
|
+
* @typedef {Object} JsonTableState
|
|
34
|
+
* @property {number} totRec - Total number of records (unfiltered, see `totRecField`)
|
|
35
|
+
* @property {number} filteredRec - Number of records matching the current search
|
|
36
|
+
* @property {ParsedRow[]} rows - All the parsed rows (current page only in server-side mode)
|
|
37
|
+
* @property {ParsedRow[]} filtered - Rows after search and sort
|
|
38
|
+
* @property {ParsedRow[]} pageRows - Rows of the current page
|
|
39
|
+
* @property {string} searchTerm - Active search term ('' = none)
|
|
40
|
+
* @property {SortDef|null} sort - Active sort (column key and direction), or null
|
|
41
|
+
* @property {number} page - Current page (1-based)
|
|
42
|
+
* @property {number} totPages - Total number of pages (1 when the pagination is disabled)
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Reason of a state update, passed in the `jt:update` event detail.
|
|
47
|
+
* @typedef {'page'|'sort'|'search'} UpdateReason
|
|
48
|
+
*/
|
|
49
|
+
|
|
12
50
|
/**
|
|
13
51
|
* Project-wide defaults, set via `JsonTable.setDefaults()`.
|
|
14
52
|
* Module-level so they are shared by every instance.
|
|
@@ -16,29 +54,37 @@ import { updateInfo } from './src/update-info.js';
|
|
|
16
54
|
*/
|
|
17
55
|
let projectDefaults = {};
|
|
18
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Empty state.
|
|
59
|
+
* @returns {JsonTableState}
|
|
60
|
+
*/
|
|
61
|
+
const emptyState = () => ({
|
|
62
|
+
totRec: 0, filteredRec: 0, rows: [], filtered: [], pageRows: [], searchTerm: '', sort: null, page: 1, totPages: 1
|
|
63
|
+
});
|
|
64
|
+
|
|
19
65
|
|
|
20
66
|
/**
|
|
21
|
-
* `<json-table>` – HTML table generator from JSON data (inline or fetched), light DOM custom element
|
|
22
|
-
*
|
|
23
|
-
* WORK IN PROGRESS: at this stage only the outer structure is generated (info section, search
|
|
24
|
-
* input, empty table with caption). Columns, rows rendering, sorting, search and pagination
|
|
25
|
-
* are not implemented yet.
|
|
67
|
+
* `<json-table>` – HTML table generator from JSON data (inline or fetched), light DOM custom element,
|
|
68
|
+
* with sorting, search and pagination (client-side, or server-side via `serverSide: true`).
|
|
26
69
|
*
|
|
27
70
|
* Parameters (see `src/defaults.js` → `JsonTableParams`) can be set as HTML attributes or via
|
|
28
71
|
* `init()`; precedence: `init()` > HTML attribute > `JsonTable.setDefaults()` > built-in default.
|
|
29
72
|
*
|
|
30
|
-
* Events
|
|
31
|
-
* `
|
|
73
|
+
* Events (both bubble, `event.detail.jsonTable` is the component instance):
|
|
74
|
+
* - `jt:ready`: dispatched once the structure has been built and the first data rendered
|
|
75
|
+
* - `jt:update`: dispatched after every page / sort / search change (`event.detail.reason`)
|
|
32
76
|
*
|
|
33
77
|
* @example
|
|
34
78
|
* // markup only
|
|
35
|
-
* // <json-table jsonurl="/api/rows.json" caption="Utenti"
|
|
79
|
+
* // <json-table jsonurl="/api/rows.json" caption="Utenti" perpage="10"
|
|
80
|
+
* // cols='[{"key":"id","dataType":"id"},{"key":"name","title":"Nome"},{"key":"amount","dataType":"euro"}]'
|
|
81
|
+
* // ></json-table>
|
|
36
82
|
*
|
|
37
83
|
* @example
|
|
38
84
|
* // script
|
|
39
85
|
* import { JsonTable } from '@massimo-cassandro/minimo/src/web-components/json-table/json-table-component.js';
|
|
40
86
|
*
|
|
41
|
-
* JsonTable.setDefaults({
|
|
87
|
+
* JsonTable.setDefaults({ classes: { table: 'table' } }); // optional, project-wide
|
|
42
88
|
*
|
|
43
89
|
* const el = document.querySelector('json-table');
|
|
44
90
|
* el.addEventListener('jt:ready', e => console.log(e.detail.jsonTable.data));
|
|
@@ -46,17 +92,37 @@ let projectDefaults = {};
|
|
|
46
92
|
* debug: false, // default: false
|
|
47
93
|
* jsonUrl: '/api/rows.json', // default: null
|
|
48
94
|
* jsonDataField: 'data', // default: 'data'
|
|
95
|
+
* totRecField: 'totRec', // default: 'totRec'
|
|
96
|
+
* filteredRecField: 'filteredRec', // default: 'filteredRec' (server-side mode)
|
|
49
97
|
* data: null, // default: null (takes precedence over jsonUrl when set)
|
|
98
|
+
* cols: [ // required
|
|
99
|
+
* { key: 'id', dataType: 'id' },
|
|
100
|
+
* { key: 'name', title: 'Nome', render: (row, tr, td) => `<a href="/users/${row.id}">${row.name}</a>` },
|
|
101
|
+
* { key: 'amount', dataType: 'euro', tfootRender: '@sum' },
|
|
102
|
+
* { key: 'active', dataType: 'bool' }
|
|
103
|
+
* ],
|
|
104
|
+
* dataTypes: {}, // default: {} (custom types, merged with the built-in ones)
|
|
50
105
|
* caption: 'Utenti', // default: null
|
|
51
106
|
* search: true, // default: true
|
|
52
|
-
*
|
|
107
|
+
* searchDebounce: 300, // default: 300 (ms)
|
|
108
|
+
* perPage: 25, // default: 25 (0 = no pagination)
|
|
109
|
+
* paginationDelta: 2, // default: 2
|
|
110
|
+
* serverSide: false, // default: false
|
|
111
|
+
* serverParams: { page: 'page', start: 'start', perPage: 'perPage', sort: 'sort', dir: 'dir', search: 'search' }, // default
|
|
112
|
+
* initialSort: { key: 'name', dir: 'asc' }, // default: null
|
|
113
|
+
* tfoot: true, // default: false (not available in server-side mode)
|
|
114
|
+
* updateFooterOnPageChange: false, // default: false
|
|
115
|
+
* infoText: null, // default: null → labels.info
|
|
116
|
+
* template: [{ slot: 'infoSection' }, { slot: 'table' }], // default
|
|
117
|
+
* locale: 'it-IT', // default: 'it-IT'
|
|
118
|
+
* currency: 'EUR', // default: 'EUR'
|
|
119
|
+
* renderNullAs: '—', // default: '—'
|
|
120
|
+
* renderZeroAs: null, // default: null
|
|
121
|
+
* renderNaNAs: '—', // default: '—'
|
|
122
|
+
* trCallback: null, // default: null
|
|
53
123
|
* tableId: null, // default: null
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
* mainWrapperExtraClass: null, // default: null
|
|
57
|
-
* outerInfoExtraClass: null, // default: null
|
|
58
|
-
* infoExtraClass: null, // default: null
|
|
59
|
-
* infoText: (shown, total) => `${shown} / ${total}` // default: Italian "Visualizzate N righe su M"
|
|
124
|
+
* classes: { table: 'table' }, // merged with the defaults (see JsonTableClasses)
|
|
125
|
+
* labels: { noRows: 'Nessun utente' } // merged with the defaults (see JsonTableLabels)
|
|
60
126
|
* });
|
|
61
127
|
*/
|
|
62
128
|
export class JsonTable extends HTMLElement {
|
|
@@ -71,9 +137,10 @@ export class JsonTable extends HTMLElement {
|
|
|
71
137
|
*
|
|
72
138
|
* @example
|
|
73
139
|
* JsonTable.setDefaults({
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
140
|
+
* perPage: 50,
|
|
141
|
+
* classes: { searchInput: 'form-control', table: 'table' }, // merged with the built-in classes
|
|
142
|
+
* labels: { searchPlaceholder: 'Cerca…' }, // merged with the built-in labels
|
|
143
|
+
* infoText: (start, end, totRec, filteredRec) => `${filteredRec} di ${totRec} record`
|
|
77
144
|
* });
|
|
78
145
|
*/
|
|
79
146
|
static setDefaults(newDefaults = {}) {
|
|
@@ -93,7 +160,7 @@ export class JsonTable extends HTMLElement {
|
|
|
93
160
|
* @returns {JsonTableParams}
|
|
94
161
|
*
|
|
95
162
|
* @example
|
|
96
|
-
* JsonTable.getDefaults().
|
|
163
|
+
* JsonTable.getDefaults().classes.tableWrapper; // → 'table-responsive'
|
|
97
164
|
*/
|
|
98
165
|
static getDefaults() {
|
|
99
166
|
return { ...defaults, ...projectDefaults };
|
|
@@ -111,13 +178,26 @@ export class JsonTable extends HTMLElement {
|
|
|
111
178
|
this._loadStarted = false;
|
|
112
179
|
/** incremented on every init()/reload()/destroy(): a pending _load() whose generation is stale gives up */
|
|
113
180
|
this._loadGeneration = 0;
|
|
181
|
+
/** incremented on every server-side request: a stale response is ignored */
|
|
182
|
+
this._requestGeneration = 0;
|
|
183
|
+
/** pending search debounce timer (see `search.js`) @type {ReturnType<typeof setTimeout>|undefined} */
|
|
184
|
+
this._searchTimer = undefined;
|
|
114
185
|
|
|
115
|
-
/** Resolved params, available after the first load. @type {JsonTableParams
|
|
116
|
-
this.params =
|
|
186
|
+
/** Resolved params, available after the first load. @type {JsonTableParams} */
|
|
187
|
+
this.params = /** @type {JsonTableParams} */ ({ ...defaults });
|
|
117
188
|
|
|
118
|
-
/** Raw rows, available after the first load. @type {Array<Object>|null} */
|
|
189
|
+
/** Raw rows, available after the first load (current page only in server-side mode). @type {Array<Object>|null} */
|
|
119
190
|
this.data = null;
|
|
120
191
|
|
|
192
|
+
/** Data types map (built-in + custom), available after the first load. @type {Object<string, DataTypeDefinition>} */
|
|
193
|
+
this.dataTypes = {};
|
|
194
|
+
|
|
195
|
+
/** Parsed visible columns, available after the first load. @type {ParsedCol[]} */
|
|
196
|
+
this.cols = [];
|
|
197
|
+
|
|
198
|
+
/** Rendering state, available after the first load. @type {JsonTableState} */
|
|
199
|
+
this.state = emptyState();
|
|
200
|
+
|
|
121
201
|
/** Generated elements, available after the first load. @type {JsonTableElements} */
|
|
122
202
|
this.elements = {};
|
|
123
203
|
}
|
|
@@ -151,8 +231,8 @@ export class JsonTable extends HTMLElement {
|
|
|
151
231
|
* @returns {void}
|
|
152
232
|
*
|
|
153
233
|
* @example
|
|
154
|
-
* // <json-table caption="Utenti" search="false"></json-table>
|
|
155
|
-
* el.init({ jsonUrl: '/api/rows.json' }); // jsonUrl from script, caption and
|
|
234
|
+
* // <json-table caption="Utenti" search="false" cols='[...]'></json-table>
|
|
235
|
+
* el.init({ jsonUrl: '/api/rows.json' }); // jsonUrl from script, caption, search and cols from attributes
|
|
156
236
|
* el.init({ jsonUrl: '/api/rows.json', search: null }); // search → default (true), the attribute is ignored
|
|
157
237
|
*/
|
|
158
238
|
init(config = {}) {
|
|
@@ -175,16 +255,20 @@ export class JsonTable extends HTMLElement {
|
|
|
175
255
|
* @returns {void}
|
|
176
256
|
*/
|
|
177
257
|
destroy() {
|
|
258
|
+
clearTimeout(this._searchTimer);
|
|
178
259
|
this._loadStarted = false;
|
|
179
260
|
this._loadGeneration++;
|
|
261
|
+
this._requestGeneration++;
|
|
180
262
|
this.elements = {};
|
|
181
263
|
this.data = null;
|
|
264
|
+
this.cols = [];
|
|
265
|
+
this.state = emptyState();
|
|
182
266
|
this.innerHTML = '';
|
|
183
267
|
}
|
|
184
268
|
|
|
185
269
|
/**
|
|
186
270
|
* Reloads the data and rebuilds the structure, optionally overriding some params
|
|
187
|
-
* (merged with the config passed to `init()`, if any).
|
|
271
|
+
* (merged with the config passed to `init()`, if any). Search, sort and page are reset.
|
|
188
272
|
*
|
|
189
273
|
* @param {Partial<JsonTableParams>} [overrides={}] - Params to override (default: {})
|
|
190
274
|
* @returns {Promise<void>}
|
|
@@ -201,11 +285,84 @@ export class JsonTable extends HTMLElement {
|
|
|
201
285
|
await this._load();
|
|
202
286
|
}
|
|
203
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Shows the given page (clamped to the available range). In server-side mode a new request
|
|
290
|
+
* is sent. Nothing happens when the page does not change.
|
|
291
|
+
*
|
|
292
|
+
* @param {number} page - Page number (1-based)
|
|
293
|
+
* @param {string|null} [focusTarget=null] - `data-page` of the pagination button to focus after the
|
|
294
|
+
* rendering (used by the pagination buttons themselves) (default: null)
|
|
295
|
+
* @returns {void}
|
|
296
|
+
*
|
|
297
|
+
* @example
|
|
298
|
+
* el.goToPage(3);
|
|
299
|
+
*/
|
|
300
|
+
goToPage(page, focusTarget = null) {
|
|
301
|
+
const requested = Math.min(Math.max(1, Math.floor(Number(page) || 1)), this.state.totPages);
|
|
302
|
+
if (requested === this.state.page) {
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
this.state.page = requested;
|
|
306
|
+
this._update('page', focusTarget);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Sorts the rows by a column (`dir` null removes the sort) and goes back to the first page.
|
|
311
|
+
* The column must exist and be sortable, otherwise the call is ignored with a console error.
|
|
312
|
+
* In server-side mode a new request is sent.
|
|
313
|
+
*
|
|
314
|
+
* @param {string} key - Column key
|
|
315
|
+
* @param {'asc'|'desc'|null} dir - Direction, or null to remove the sort
|
|
316
|
+
* @returns {void}
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* el.setSort('name', 'desc');
|
|
320
|
+
* el.setSort('name', null); // original order
|
|
321
|
+
*/
|
|
322
|
+
setSort(key, dir) {
|
|
323
|
+
if (dir != null) {
|
|
324
|
+
const col = this.cols.find(c => c.key === key);
|
|
325
|
+
if (!col || !col.sortable) {
|
|
326
|
+
// eslint-disable-next-line no-console
|
|
327
|
+
console.error(`[json-table] setSort: colonna \`${key}\` inesistente o non ordinabile`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
this.state.sort = dir == null ? null : { key, dir };
|
|
332
|
+
this.state.page = 1;
|
|
333
|
+
this._update('sort');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Filters the rows by a search term (every whitespace-separated word must match, case-insensitive)
|
|
338
|
+
* and goes back to the first page; an empty term removes the filter. The search input, when
|
|
339
|
+
* present, is kept in sync. In server-side mode a new request is sent.
|
|
340
|
+
*
|
|
341
|
+
* @param {string} term - Search term
|
|
342
|
+
* @returns {void}
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* el.setSearch('mario');
|
|
346
|
+
* el.setSearch('');
|
|
347
|
+
*/
|
|
348
|
+
setSearch(term) {
|
|
349
|
+
const value = String(term ?? '');
|
|
350
|
+
if (this.elements.searchInput && this.elements.searchInput.value !== value) {
|
|
351
|
+
this.elements.searchInput.value = value;
|
|
352
|
+
}
|
|
353
|
+
if (value.trim() === this.state.searchTerm) {
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
this.state.searchTerm = value.trim();
|
|
357
|
+
this.state.page = 1;
|
|
358
|
+
this._update('search');
|
|
359
|
+
}
|
|
360
|
+
|
|
204
361
|
|
|
205
362
|
// ─── Load & build ───────────────────────────────────────────────────────────
|
|
206
363
|
|
|
207
364
|
/**
|
|
208
|
-
* Resolves params, retrieves the data and builds the structure.
|
|
365
|
+
* Resolves params, parses the columns, retrieves the data and builds the structure.
|
|
209
366
|
* Guarded against concurrent/stale calls via `_loadStarted` and `_loadGeneration`.
|
|
210
367
|
* @returns {Promise<void>}
|
|
211
368
|
*/
|
|
@@ -214,20 +371,51 @@ export class JsonTable extends HTMLElement {
|
|
|
214
371
|
return;
|
|
215
372
|
}
|
|
216
373
|
this._loadStarted = true;
|
|
374
|
+
clearTimeout(this._searchTimer);
|
|
217
375
|
const generation = this._loadGeneration;
|
|
218
376
|
|
|
219
377
|
const params = resolveParams(this, this._config, projectDefaults);
|
|
220
378
|
this.params = params;
|
|
379
|
+
this._validateParams(params);
|
|
221
380
|
|
|
222
381
|
// loading placeholder (minimo spinner)
|
|
223
|
-
|
|
382
|
+
domBuilder([
|
|
383
|
+
{
|
|
384
|
+
className: 'spinner',
|
|
385
|
+
children: [{ tag: 'span', className: 'visually-hidden', content: params.labels.loading }]
|
|
386
|
+
}
|
|
387
|
+
], this, { emptyParent: true });
|
|
388
|
+
|
|
389
|
+
// columns parsing (configuration errors are reported and stop the rendering)
|
|
390
|
+
try {
|
|
391
|
+
this.dataTypes = buildDataTypes(params);
|
|
392
|
+
this.cols = parseCols(params.cols, this.dataTypes, params);
|
|
393
|
+
} catch (err) {
|
|
394
|
+
// eslint-disable-next-line no-console
|
|
395
|
+
console.error(err);
|
|
396
|
+
this.innerHTML = '';
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
224
399
|
|
|
225
|
-
|
|
226
|
-
|
|
400
|
+
// initial state
|
|
401
|
+
this.state = emptyState();
|
|
402
|
+
if (params.initialSort && typeof params.initialSort === 'object') {
|
|
403
|
+
const { key, dir } = params.initialSort;
|
|
404
|
+
const col = this.cols.find(c => c.key === key);
|
|
405
|
+
if (col && col.sortable && (dir === 'asc' || dir === 'desc')) {
|
|
406
|
+
this.state.sort = { key, dir };
|
|
407
|
+
} else {
|
|
408
|
+
// eslint-disable-next-line no-console
|
|
409
|
+
console.error(`[json-table] initialSort: colonna \`${key}\` inesistente o non ordinabile, oppure direzione non valida (${dir})`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/** @type {import('./src/get-data.js').DataResult|null} */
|
|
414
|
+
let result = null;
|
|
227
415
|
let failed = false;
|
|
228
416
|
|
|
229
417
|
try {
|
|
230
|
-
|
|
418
|
+
result = await getData(params, params.serverSide ? this._serverRequest() : null);
|
|
231
419
|
} catch (err) {
|
|
232
420
|
failed = true;
|
|
233
421
|
// eslint-disable-next-line no-console
|
|
@@ -244,7 +432,7 @@ export class JsonTable extends HTMLElement {
|
|
|
244
432
|
return;
|
|
245
433
|
}
|
|
246
434
|
|
|
247
|
-
if (
|
|
435
|
+
if (result === null) {
|
|
248
436
|
// no source: silent when created from markup without attributes (init() may follow)
|
|
249
437
|
if (this._initCalledProgrammatically) {
|
|
250
438
|
// eslint-disable-next-line no-console
|
|
@@ -254,17 +442,23 @@ export class JsonTable extends HTMLElement {
|
|
|
254
442
|
return;
|
|
255
443
|
}
|
|
256
444
|
|
|
257
|
-
this.
|
|
258
|
-
|
|
445
|
+
this._setData(result);
|
|
446
|
+
|
|
447
|
+
this.elements = mainBuilder(this);
|
|
448
|
+
setSortListener(this);
|
|
449
|
+
setSearchListener(this);
|
|
259
450
|
|
|
260
|
-
|
|
261
|
-
|
|
451
|
+
this._computeState();
|
|
452
|
+
this._render();
|
|
262
453
|
|
|
263
454
|
if (params.debug) {
|
|
264
455
|
/* eslint-disable no-console */
|
|
265
|
-
console.groupCollapsed('[json-table] params, data & elements', this);
|
|
456
|
+
console.groupCollapsed('[json-table] params, cols, data, state & elements', this);
|
|
266
457
|
console.log('params', params);
|
|
267
|
-
console.log('
|
|
458
|
+
console.log('dataTypes', this.dataTypes);
|
|
459
|
+
console.log('cols', this.cols);
|
|
460
|
+
console.log('data', this.data);
|
|
461
|
+
console.log('state', this.state);
|
|
268
462
|
console.log('elements', this.elements);
|
|
269
463
|
console.groupEnd();
|
|
270
464
|
/* eslint-enable no-console */
|
|
@@ -275,6 +469,161 @@ export class JsonTable extends HTMLElement {
|
|
|
275
469
|
Promise.resolve().then(() => this.dispatchEvent(event));
|
|
276
470
|
}
|
|
277
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Normalizes the resolved params that depend on each other, reporting the inconsistencies:
|
|
474
|
+
* `perPage`/`paginationDelta` must be non-negative numbers, `serverSide` requires `jsonUrl`
|
|
475
|
+
* (ignored with inline `data`) and disables `tfoot` (the aggregates would be computed on the
|
|
476
|
+
* current page only).
|
|
477
|
+
* @param {JsonTableParams} params - Resolved params (mutated)
|
|
478
|
+
* @returns {void}
|
|
479
|
+
*/
|
|
480
|
+
_validateParams(params) {
|
|
481
|
+
/* eslint-disable no-console */
|
|
482
|
+
const perPage = Number(params.perPage);
|
|
483
|
+
params.perPage = Number.isFinite(perPage) && perPage >= 0 ? Math.floor(perPage) : defaults.perPage;
|
|
484
|
+
|
|
485
|
+
const delta = Number(params.paginationDelta);
|
|
486
|
+
params.paginationDelta = Number.isFinite(delta) && delta >= 0 ? Math.floor(delta) : defaults.paginationDelta;
|
|
487
|
+
|
|
488
|
+
if (params.serverSide && (params.data != null || !params.jsonUrl)) {
|
|
489
|
+
console.warn('[json-table] `serverSide` richiede `jsonUrl` (senza `data`): modalità server-side ignorata');
|
|
490
|
+
params.serverSide = false;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (params.serverSide && params.tfoot) {
|
|
494
|
+
console.warn('[json-table] `tfoot` non è disponibile in modalità server-side (i dati sono paginati dal server): tfoot ignorato');
|
|
495
|
+
params.tfoot = false;
|
|
496
|
+
}
|
|
497
|
+
/* eslint-enable no-console */
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Current server-side request state (see `get-data.js` → `ServerRequest`).
|
|
502
|
+
* @returns {import('./src/get-data.js').ServerRequest}
|
|
503
|
+
*/
|
|
504
|
+
_serverRequest() {
|
|
505
|
+
const { state, params } = this;
|
|
506
|
+
return {
|
|
507
|
+
page: state.page,
|
|
508
|
+
perPage: params.perPage > 0 ? params.perPage : 0,
|
|
509
|
+
sort: state.sort,
|
|
510
|
+
search: state.searchTerm
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Stores a data result: raw rows, parsed rows and totals.
|
|
516
|
+
* @param {import('./src/get-data.js').DataResult} result
|
|
517
|
+
* @returns {void}
|
|
518
|
+
*/
|
|
519
|
+
_setData(result) {
|
|
520
|
+
this.data = result.rows;
|
|
521
|
+
this.state.rows = parseRows(result.rows, this.cols, this.params);
|
|
522
|
+
this.state.totRec = result.totRec;
|
|
523
|
+
this.state.filteredRec = result.filteredRec;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/**
|
|
527
|
+
* Computes `filtered`, `pageRows`, `filteredRec`, `totPages` and clamps `page` from `rows`,
|
|
528
|
+
* `searchTerm`, `sort` and `page`.
|
|
529
|
+
*
|
|
530
|
+
* Client-side mode: search and sort are applied to `rows`, then the current page is sliced.
|
|
531
|
+
* Server-side mode: `rows` already is the requested page, `filteredRec` comes from the JSON.
|
|
532
|
+
* @returns {void}
|
|
533
|
+
*/
|
|
534
|
+
_computeState() {
|
|
535
|
+
const { state, params } = this;
|
|
536
|
+
|
|
537
|
+
if (params.serverSide) {
|
|
538
|
+
state.filtered = state.rows;
|
|
539
|
+
state.pageRows = state.rows;
|
|
540
|
+
state.totPages = calcTotPages(state.filteredRec, params.perPage);
|
|
541
|
+
state.page = Math.min(Math.max(1, state.page), state.totPages);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const filtered = filterRows(state.rows, state.searchTerm);
|
|
546
|
+
state.filtered = state.sort ? sortRows(filtered, state.sort.key, state.sort.dir, params.locale) : filtered;
|
|
547
|
+
state.filteredRec = state.filtered.length;
|
|
548
|
+
state.totPages = calcTotPages(state.filteredRec, params.perPage);
|
|
549
|
+
state.page = Math.min(Math.max(1, state.page), state.totPages);
|
|
550
|
+
state.pageRows = params.perPage > 0
|
|
551
|
+
? state.filtered.slice((state.page - 1) * params.perPage, state.page * params.perPage)
|
|
552
|
+
: state.filtered;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Applies a state change (page, sort or search): recomputes the state and re-renders, or, in
|
|
557
|
+
* server-side mode, sends a new request (stale responses are ignored) and renders its result.
|
|
558
|
+
* Dispatches `jt:update` at the end.
|
|
559
|
+
*
|
|
560
|
+
* @param {UpdateReason} reason - What changed
|
|
561
|
+
* @param {string|null} [focusTarget=null] - Passed to `renderPagination()` (default: null)
|
|
562
|
+
* @returns {Promise<void>}
|
|
563
|
+
*/
|
|
564
|
+
async _update(reason, focusTarget = null) {
|
|
565
|
+
|
|
566
|
+
if (this.params.serverSide) {
|
|
567
|
+
const generation = ++this._requestGeneration;
|
|
568
|
+
const loadGeneration = this._loadGeneration;
|
|
569
|
+
const requestedPage = this.state.page;
|
|
570
|
+
this.elements.wrapper?.setAttribute('aria-busy', 'true');
|
|
571
|
+
|
|
572
|
+
/** @type {import('./src/get-data.js').DataResult|null} */
|
|
573
|
+
let result = null;
|
|
574
|
+
try {
|
|
575
|
+
result = await getData(this.params, this._serverRequest());
|
|
576
|
+
} catch (err) {
|
|
577
|
+
// eslint-disable-next-line no-console
|
|
578
|
+
console.error(err);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (generation !== this._requestGeneration || loadGeneration !== this._loadGeneration) {
|
|
582
|
+
return; // superseded by a newer request or by a reload/destroy
|
|
583
|
+
}
|
|
584
|
+
this.elements.wrapper?.removeAttribute('aria-busy');
|
|
585
|
+
|
|
586
|
+
if (!result) {
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
this._setData(result);
|
|
590
|
+
this._computeState();
|
|
591
|
+
|
|
592
|
+
// the requested page no longer exists (the data set shrank since the last request):
|
|
593
|
+
// `_computeState` clamped the page, fetch the last available one
|
|
594
|
+
if (!this.state.rows.length && this.state.filteredRec > 0 && this.state.page !== requestedPage) {
|
|
595
|
+
this._update(reason, focusTarget);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
} else {
|
|
600
|
+
this._computeState();
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this._render(reason, focusTarget);
|
|
604
|
+
|
|
605
|
+
this.dispatchEvent(new CustomEvent('jt:update', { detail: { jsonTable: this, reason }, bubbles: true }));
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Renders the parts depending on the state: body rows, footer, sort indicators, pagination and
|
|
610
|
+
* info text. The footer is skipped on page changes when `updateFooterOnPageChange` is false
|
|
611
|
+
* (its content would not change).
|
|
612
|
+
*
|
|
613
|
+
* @param {UpdateReason|null} [reason=null] - What changed (null on the first rendering) (default: null)
|
|
614
|
+
* @param {string|null} [focusTarget=null] - Passed to `renderPagination()` (default: null)
|
|
615
|
+
* @returns {void}
|
|
616
|
+
*/
|
|
617
|
+
_render(reason = null, focusTarget = null) {
|
|
618
|
+
renderTbody(this);
|
|
619
|
+
if (reason !== 'page' || this.params.updateFooterOnPageChange) {
|
|
620
|
+
renderTfoot(this);
|
|
621
|
+
}
|
|
622
|
+
applySortState(this);
|
|
623
|
+
renderPagination(this, focusTarget);
|
|
624
|
+
updateInfo(this);
|
|
625
|
+
}
|
|
626
|
+
|
|
278
627
|
} // end component
|
|
279
628
|
|
|
280
629
|
|