@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.
Files changed (88) hide show
  1. package/README.md +2 -1
  2. package/dev-tools/starter-kit/source_files/_private/pwd.md +16 -0
  3. package/dev-tools/starter-kit/source_files/_private/sf.md +12 -0
  4. package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +2 -1
  5. package/dev-tools/starter-kit/source_files/app/assets/api_htaccess +20 -0
  6. package/dev-tools/starter-kit/source_files/{layout-critical.css → app/css/layout.critical.css} +1 -1
  7. package/dev-tools/starter-kit/source_files/app/css/layout.css +0 -0
  8. package/dev-tools/starter-kit/source_files/app/index.js +1 -0
  9. package/dev-tools/starter-kit/{package-tpl.json → source_files/root/package-tpl.json} +1 -1
  10. package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +1 -1
  11. package/dev-tools/starter-kit/source_files/root/webpack-config-modules/inline-critical-css.mjs +41 -0
  12. package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-setup.mjs +1 -1
  13. package/dev-tools/starter-kit/{webpack.config.mjs → source_files/root/webpack.config.mjs} +56 -38
  14. package/dev-tools/starter-kit/starter-install.sh +233 -81
  15. package/dev-tools/stylelint-config/README.md +45 -0
  16. package/dev-tools/stylelint-config/index.js +184 -0
  17. package/dev-tools/stylelint-config/test/test.module.scss +9 -0
  18. package/package.json +11 -2
  19. package/snippets/php/iviewer.php +488 -0
  20. package/snippets/twig-symfony/ViewerController.php +549 -0
  21. package/src/custom-properties.css +36 -0
  22. package/src/minimo.css +3 -0
  23. package/src/utilities/dom-builder/dom-builder.js +9 -5
  24. package/src/web-components/json-table/json-table-component.js +632 -0
  25. package/src/web-components/json-table/json-table-component.module.css +247 -0
  26. package/src/web-components/json-table/json-table.minimo.tokens.mjs +243 -0
  27. package/src/web-components/json-table/readme.md +764 -0
  28. package/src/web-components/json-table/src/cell-content.js +136 -0
  29. package/src/web-components/json-table/src/content-utils.js +103 -0
  30. package/src/web-components/json-table/src/data-types.js +280 -0
  31. package/src/web-components/json-table/src/defaults.js +424 -0
  32. package/src/web-components/json-table/src/get-data.js +152 -0
  33. package/src/web-components/json-table/src/icons.js +80 -0
  34. package/src/web-components/json-table/src/info-section.js +107 -0
  35. package/src/web-components/json-table/src/main-builder.js +218 -0
  36. package/src/web-components/json-table/src/pagination.js +253 -0
  37. package/src/web-components/json-table/src/parse-cols.js +113 -0
  38. package/src/web-components/json-table/src/parse-rows.js +81 -0
  39. package/src/web-components/json-table/src/resolve-params.js +166 -0
  40. package/src/web-components/json-table/src/search.js +71 -0
  41. package/src/web-components/json-table/src/sorting.js +111 -0
  42. package/src/web-components/json-table/src/table-body.js +109 -0
  43. package/src/web-components/json-table/src/table-builder.js +152 -0
  44. package/src/web-components/json-table/src/table-tfoot.js +56 -0
  45. package/src/web-components/json-table/src/table-thead.js +183 -0
  46. package/src/web-components/json-table/src/update-info.js +67 -0
  47. package/src/web-components/s-datatable-component/{docs.md → readme.md} +67 -3
  48. package/src/web-components/s-datatable-component/s-datatable-component.js +1 -1
  49. package/types/global.d.ts +17 -3
  50. package/dev-tools/starter-kit/source_files/app_index.js +0 -1
  51. package/snippets/config-templates/_htaccess +0 -69
  52. package/snippets/config-templates/robots.txt +0 -5
  53. package/snippets/config-templates/rollup.config.mjs +0 -115
  54. /package/dev-tools/starter-kit/source_files/{_root_robots.txt → app/assets/robots.txt} +0 -0
  55. /package/dev-tools/starter-kit/source_files/{_root_htaccess → app/assets/root_htaccess} +0 -0
  56. /package/{src → dev-tools/starter-kit/source_files/app/css}/fonts.css +0 -0
  57. /package/dev-tools/starter-kit/source_files/{favicons-readme.md → app/favicons/favicons-readme.md} +0 -0
  58. /package/dev-tools/starter-kit/{webpack-template.ejs → source_files/app/tpl/index.ejs} +0 -0
  59. /package/dev-tools/starter-kit/source_files/{__project__.code-workspace → root/__project__.code-workspace} +0 -0
  60. /package/dev-tools/starter-kit/source_files/{_browserslistrc → root/_browserslistrc} +0 -0
  61. /package/dev-tools/starter-kit/source_files/{_editorconfig → root/_editorconfig} +0 -0
  62. /package/dev-tools/starter-kit/source_files/{_gitignore → root/_gitignore} +0 -0
  63. /package/dev-tools/starter-kit/source_files/{_prettierrc → root/_prettierrc} +0 -0
  64. /package/dev-tools/starter-kit/source_files/{eslint.config.mjs → root/eslint.config.mjs} +0 -0
  65. /package/dev-tools/starter-kit/source_files/{jsconfig.json → root/jsconfig.json} +0 -0
  66. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/css-rules.mjs +0 -0
  67. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/get-jsConfig-aliases.mjs +0 -0
  68. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/mini-svg-data-uri-loader.cjs +0 -0
  69. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/postcss.config.mjs +0 -0
  70. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-variables-safelist.mjs +0 -0
  71. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svg-rules.mjs +0 -0
  72. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svgo.config.mjs +0 -0
  73. /package/snippets/{config-templates/_github → _github}/dependabot.yml +0 -0
  74. /package/snippets/{error-pages/error-pages.js → error-pages.js} +0 -0
  75. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.jsx +0 -0
  76. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.module.scss +0 -0
  77. /package/snippets/{config-templates → root}/_hintrc.cjs +0 -0
  78. /package/snippets/{config-templates → root}/_npmignore +0 -0
  79. /package/snippets/{config-templates → root}/_npmrc +0 -0
  80. /package/snippets/{config-templates → root}/_stylelintignore +0 -0
  81. /package/snippets/{config-templates → root}/babel.config--react.json +0 -0
  82. /package/snippets/{config-templates → root}/babel.config--solidJS.json +0 -0
  83. /package/snippets/{ImgViewerController.php → twig-symfony/ImgViewerController.php} +0 -0
  84. /package/snippets/{UnsplashController.php → twig-symfony/UnsplashController.php} +0 -0
  85. /package/snippets/{config-templates → twig-symfony}/_env.local +0 -0
  86. /package/snippets/{error-pages → twig-symfony}/bundles/TwigBundle/Exception/error.html.twig +0 -0
  87. /package/snippets/{flash-messages.html.twig → twig-symfony/flash-messages.html.twig} +0 -0
  88. /package/snippets/{minimo-form-layout.html.twig → twig-symfony/minimo-form-layout.html.twig} +0 -0
@@ -0,0 +1,632 @@
1
+ /*! minimo - json-table */
2
+
3
+ import { defaults } from './src/defaults.js';
4
+ import { resolveParams } from './src/resolve-params.js';
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';
9
+ import { mainBuilder } from './src/main-builder.js';
10
+ import { renderTbody } from './src/table-body.js';
11
+ import { renderTfoot } from './src/table-tfoot.js';
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';
18
+
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 */
24
+ /** @typedef {import('./src/main-builder.js').JsonTableElements} JsonTableElements */
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
+
50
+ /**
51
+ * Project-wide defaults, set via `JsonTable.setDefaults()`.
52
+ * Module-level so they are shared by every instance.
53
+ * @type {Partial<JsonTableParams>}
54
+ */
55
+ let projectDefaults = {};
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
+
65
+
66
+ /**
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`).
69
+ *
70
+ * Parameters (see `src/defaults.js` → `JsonTableParams`) can be set as HTML attributes or via
71
+ * `init()`; precedence: `init()` > HTML attribute > `JsonTable.setDefaults()` > built-in default.
72
+ *
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`)
76
+ *
77
+ * @example
78
+ * // markup only
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>
82
+ *
83
+ * @example
84
+ * // script
85
+ * import { JsonTable } from '@massimo-cassandro/minimo/src/web-components/json-table/json-table-component.js';
86
+ *
87
+ * JsonTable.setDefaults({ classes: { table: 'table' } }); // optional, project-wide
88
+ *
89
+ * const el = document.querySelector('json-table');
90
+ * el.addEventListener('jt:ready', e => console.log(e.detail.jsonTable.data));
91
+ * el.init({
92
+ * debug: false, // default: false
93
+ * jsonUrl: '/api/rows.json', // default: null
94
+ * jsonDataField: 'data', // default: 'data'
95
+ * totRecField: 'totRec', // default: 'totRec'
96
+ * filteredRecField: 'filteredRec', // default: 'filteredRec' (server-side mode)
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)
105
+ * caption: 'Utenti', // default: null
106
+ * search: true, // default: true
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
123
+ * tableId: null, // default: null
124
+ * classes: { table: 'table' }, // merged with the defaults (see JsonTableClasses)
125
+ * labels: { noRows: 'Nessun utente' } // merged with the defaults (see JsonTableLabels)
126
+ * });
127
+ */
128
+ export class JsonTable extends HTMLElement {
129
+
130
+ /**
131
+ * Sets project-wide defaults, shared by every instance created afterwards
132
+ * (instances already rendered are not updated). Call it once, before the instances
133
+ * are created, e.g. in a shared script. Values are merged with the previous ones.
134
+ *
135
+ * @param {Partial<JsonTableParams>} [newDefaults={}] - Parameters to override (default: {})
136
+ * @returns {void}
137
+ *
138
+ * @example
139
+ * JsonTable.setDefaults({
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`
144
+ * });
145
+ */
146
+ static setDefaults(newDefaults = {}) {
147
+ projectDefaults = { ...projectDefaults, ...newDefaults };
148
+ }
149
+
150
+ /**
151
+ * Clears the project-wide defaults set via `setDefaults()`.
152
+ * @returns {void}
153
+ */
154
+ static resetDefaults() {
155
+ projectDefaults = {};
156
+ }
157
+
158
+ /**
159
+ * Returns the built-in defaults merged with the project-wide ones.
160
+ * @returns {JsonTableParams}
161
+ *
162
+ * @example
163
+ * JsonTable.getDefaults().classes.tableWrapper; // → 'table-responsive'
164
+ */
165
+ static getDefaults() {
166
+ return { ...defaults, ...projectDefaults };
167
+ }
168
+
169
+
170
+ constructor() {
171
+ super();
172
+ // light DOM: no attachShadow
173
+
174
+ /** @type {Partial<JsonTableParams>|null} config passed via init()/reload() */
175
+ this._config = null;
176
+ this._initCalledProgrammatically = false;
177
+ this._isConnected = false;
178
+ this._loadStarted = false;
179
+ /** incremented on every init()/reload()/destroy(): a pending _load() whose generation is stale gives up */
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;
185
+
186
+ /** Resolved params, available after the first load. @type {JsonTableParams} */
187
+ this.params = /** @type {JsonTableParams} */ ({ ...defaults });
188
+
189
+ /** Raw rows, available after the first load (current page only in server-side mode). @type {Array<Object>|null} */
190
+ this.data = null;
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
+
201
+ /** Generated elements, available after the first load. @type {JsonTableElements} */
202
+ this.elements = {};
203
+ }
204
+
205
+
206
+ // ─── Lifecycle ──────────────────────────────────────────────────────────────
207
+
208
+ connectedCallback() {
209
+ this._isConnected = true;
210
+ if (!this._loadStarted) {
211
+ this._load();
212
+ }
213
+ }
214
+
215
+ disconnectedCallback() {
216
+ this._isConnected = false;
217
+ }
218
+
219
+
220
+ // ─── Public API ─────────────────────────────────────────────────────────────
221
+
222
+ /**
223
+ * Starts the component via script, passing the configuration as an object.
224
+ * Can be called before or after the element is attached to the DOM.
225
+ *
226
+ * Parameters not included in `config` are read from the corresponding HTML attribute
227
+ * (if present), then from the project defaults, then from the built-in defaults.
228
+ * To explicitly ignore an existing attribute pass `null` in the config.
229
+ *
230
+ * @param {Partial<JsonTableParams>} [config={}] - See `JsonTableParams` (default: {})
231
+ * @returns {void}
232
+ *
233
+ * @example
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
236
+ * el.init({ jsonUrl: '/api/rows.json', search: null }); // search → default (true), the attribute is ignored
237
+ */
238
+ init(config = {}) {
239
+ this._config = config;
240
+ this._initCalledProgrammatically = true;
241
+
242
+ // an explicit init() must always restart, even if connectedCallback already
243
+ // ran a premature _load() (e.g. when domBuilder appends the element before init())
244
+ this._loadStarted = false;
245
+ this._loadGeneration++;
246
+
247
+ if (this._isConnected) {
248
+ this._load();
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Empties the component and resets its state. `init()` can be called again afterwards
254
+ * to re-initialize it with new params, without recreating the element.
255
+ * @returns {void}
256
+ */
257
+ destroy() {
258
+ clearTimeout(this._searchTimer);
259
+ this._loadStarted = false;
260
+ this._loadGeneration++;
261
+ this._requestGeneration++;
262
+ this.elements = {};
263
+ this.data = null;
264
+ this.cols = [];
265
+ this.state = emptyState();
266
+ this.innerHTML = '';
267
+ }
268
+
269
+ /**
270
+ * Reloads the data and rebuilds the structure, optionally overriding some params
271
+ * (merged with the config passed to `init()`, if any). Search, sort and page are reset.
272
+ *
273
+ * @param {Partial<JsonTableParams>} [overrides={}] - Params to override (default: {})
274
+ * @returns {Promise<void>}
275
+ *
276
+ * @example
277
+ * await el.reload(); // same source
278
+ * await el.reload({ jsonUrl: '/api/rows.json?y=2025' }); // new URL
279
+ * await el.reload({ data: [{ id: 1 }] }); // inline data (takes precedence over jsonUrl)
280
+ */
281
+ async reload(overrides = {}) {
282
+ this._config = { ...(this._config ?? {}), ...overrides };
283
+ this._loadStarted = false;
284
+ this._loadGeneration++;
285
+ await this._load();
286
+ }
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
+
361
+
362
+ // ─── Load & build ───────────────────────────────────────────────────────────
363
+
364
+ /**
365
+ * Resolves params, parses the columns, retrieves the data and builds the structure.
366
+ * Guarded against concurrent/stale calls via `_loadStarted` and `_loadGeneration`.
367
+ * @returns {Promise<void>}
368
+ */
369
+ async _load() {
370
+ if (this._loadStarted) {
371
+ return;
372
+ }
373
+ this._loadStarted = true;
374
+ clearTimeout(this._searchTimer);
375
+ const generation = this._loadGeneration;
376
+
377
+ const params = resolveParams(this, this._config, projectDefaults);
378
+ this.params = params;
379
+ this._validateParams(params);
380
+
381
+ // loading placeholder (minimo spinner)
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
+ }
399
+
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;
415
+ let failed = false;
416
+
417
+ try {
418
+ result = await getData(params, params.serverSide ? this._serverRequest() : null);
419
+ } catch (err) {
420
+ failed = true;
421
+ // eslint-disable-next-line no-console
422
+ console.error(err);
423
+ }
424
+
425
+ // a newer init()/reload()/destroy() superseded this load
426
+ if (generation !== this._loadGeneration) {
427
+ return;
428
+ }
429
+
430
+ if (failed) {
431
+ this.innerHTML = '';
432
+ return;
433
+ }
434
+
435
+ if (result === null) {
436
+ // no source: silent when created from markup without attributes (init() may follow)
437
+ if (this._initCalledProgrammatically) {
438
+ // eslint-disable-next-line no-console
439
+ console.error('[json-table] Nessuna sorgente dati: specificare `data` o `jsonUrl`.');
440
+ }
441
+ this.innerHTML = '';
442
+ return;
443
+ }
444
+
445
+ this._setData(result);
446
+
447
+ this.elements = mainBuilder(this);
448
+ setSortListener(this);
449
+ setSearchListener(this);
450
+
451
+ this._computeState();
452
+ this._render();
453
+
454
+ if (params.debug) {
455
+ /* eslint-disable no-console */
456
+ console.groupCollapsed('[json-table] params, cols, data, state & elements', this);
457
+ console.log('params', params);
458
+ console.log('dataTypes', this.dataTypes);
459
+ console.log('cols', this.cols);
460
+ console.log('data', this.data);
461
+ console.log('state', this.state);
462
+ console.log('elements', this.elements);
463
+ console.groupEnd();
464
+ /* eslint-enable no-console */
465
+ }
466
+
467
+ // dispatched in the next microtask so listeners registered right after init() still catch it
468
+ const event = new CustomEvent('jt:ready', { detail: { jsonTable: this }, bubbles: true });
469
+ Promise.resolve().then(() => this.dispatchEvent(event));
470
+ }
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
+
627
+ } // end component
628
+
629
+
630
+ if (!customElements.get('json-table')) {
631
+ customElements.define('json-table', JsonTable);
632
+ }