@massimo-cassandro/minimo 1.32.4 → 1.33.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dev-tools/starter-kit/source_files/_private/pwd.md +16 -0
- package/dev-tools/starter-kit/source_files/_private/sf.md +12 -0
- package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +2 -1
- package/dev-tools/starter-kit/source_files/app/assets/api_htaccess +20 -0
- package/dev-tools/starter-kit/source_files/{layout-critical.css → app/css/layout.critical.css} +1 -1
- package/dev-tools/starter-kit/source_files/app/css/layout.css +0 -0
- package/dev-tools/starter-kit/source_files/app/index.js +1 -0
- package/dev-tools/starter-kit/{package-tpl.json → source_files/root/package-tpl.json} +1 -1
- package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +1 -1
- package/dev-tools/starter-kit/source_files/root/webpack-config-modules/inline-critical-css.mjs +41 -0
- package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-setup.mjs +1 -1
- package/dev-tools/starter-kit/{webpack.config.mjs → source_files/root/webpack.config.mjs} +56 -38
- package/dev-tools/starter-kit/starter-install.sh +233 -81
- package/dev-tools/stylelint-config/README.md +45 -0
- package/dev-tools/stylelint-config/index.js +184 -0
- package/dev-tools/stylelint-config/test/test.module.scss +9 -0
- package/package.json +11 -2
- package/snippets/php/iviewer.php +488 -0
- package/snippets/twig-symfony/ViewerController.php +549 -0
- package/src/custom-properties.css +36 -0
- package/src/minimo.css +3 -0
- package/src/utilities/dom-builder/dom-builder.js +9 -5
- package/src/web-components/json-table/json-table-component.js +632 -0
- package/src/web-components/json-table/json-table-component.module.css +247 -0
- package/src/web-components/json-table/json-table.minimo.tokens.mjs +243 -0
- package/src/web-components/json-table/readme.md +764 -0
- package/src/web-components/json-table/src/cell-content.js +136 -0
- package/src/web-components/json-table/src/content-utils.js +103 -0
- package/src/web-components/json-table/src/data-types.js +280 -0
- package/src/web-components/json-table/src/defaults.js +424 -0
- package/src/web-components/json-table/src/get-data.js +152 -0
- package/src/web-components/json-table/src/icons.js +80 -0
- package/src/web-components/json-table/src/info-section.js +107 -0
- package/src/web-components/json-table/src/main-builder.js +218 -0
- package/src/web-components/json-table/src/pagination.js +253 -0
- package/src/web-components/json-table/src/parse-cols.js +113 -0
- package/src/web-components/json-table/src/parse-rows.js +81 -0
- package/src/web-components/json-table/src/resolve-params.js +166 -0
- package/src/web-components/json-table/src/search.js +71 -0
- package/src/web-components/json-table/src/sorting.js +111 -0
- package/src/web-components/json-table/src/table-body.js +109 -0
- package/src/web-components/json-table/src/table-builder.js +152 -0
- package/src/web-components/json-table/src/table-tfoot.js +56 -0
- package/src/web-components/json-table/src/table-thead.js +183 -0
- package/src/web-components/json-table/src/update-info.js +67 -0
- package/src/web-components/s-datatable-component/{docs.md → readme.md} +67 -3
- package/src/web-components/s-datatable-component/s-datatable-component.js +1 -1
- package/types/global.d.ts +17 -3
- package/dev-tools/starter-kit/source_files/app_index.js +0 -1
- package/snippets/config-templates/_htaccess +0 -69
- package/snippets/config-templates/robots.txt +0 -5
- package/snippets/config-templates/rollup.config.mjs +0 -115
- /package/dev-tools/starter-kit/source_files/{_root_robots.txt → app/assets/robots.txt} +0 -0
- /package/dev-tools/starter-kit/source_files/{_root_htaccess → app/assets/root_htaccess} +0 -0
- /package/{src → dev-tools/starter-kit/source_files/app/css}/fonts.css +0 -0
- /package/dev-tools/starter-kit/source_files/{favicons-readme.md → app/favicons/favicons-readme.md} +0 -0
- /package/dev-tools/starter-kit/{webpack-template.ejs → source_files/app/tpl/index.ejs} +0 -0
- /package/dev-tools/starter-kit/source_files/{__project__.code-workspace → root/__project__.code-workspace} +0 -0
- /package/dev-tools/starter-kit/source_files/{_browserslistrc → root/_browserslistrc} +0 -0
- /package/dev-tools/starter-kit/source_files/{_editorconfig → root/_editorconfig} +0 -0
- /package/dev-tools/starter-kit/source_files/{_gitignore → root/_gitignore} +0 -0
- /package/dev-tools/starter-kit/source_files/{_prettierrc → root/_prettierrc} +0 -0
- /package/dev-tools/starter-kit/source_files/{eslint.config.mjs → root/eslint.config.mjs} +0 -0
- /package/dev-tools/starter-kit/source_files/{jsconfig.json → root/jsconfig.json} +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/css-rules.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/get-jsConfig-aliases.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/mini-svg-data-uri-loader.cjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/postcss.config.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-variables-safelist.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svg-rules.mjs +0 -0
- /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svgo.config.mjs +0 -0
- /package/snippets/{config-templates/_github → _github}/dependabot.yml +0 -0
- /package/snippets/{error-pages/error-pages.js → error-pages.js} +0 -0
- /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.jsx +0 -0
- /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.module.scss +0 -0
- /package/snippets/{config-templates → root}/_hintrc.cjs +0 -0
- /package/snippets/{config-templates → root}/_npmignore +0 -0
- /package/snippets/{config-templates → root}/_npmrc +0 -0
- /package/snippets/{config-templates → root}/_stylelintignore +0 -0
- /package/snippets/{config-templates → root}/babel.config--react.json +0 -0
- /package/snippets/{config-templates → root}/babel.config--solidJS.json +0 -0
- /package/snippets/{ImgViewerController.php → twig-symfony/ImgViewerController.php} +0 -0
- /package/snippets/{UnsplashController.php → twig-symfony/UnsplashController.php} +0 -0
- /package/snippets/{config-templates → twig-symfony}/_env.local +0 -0
- /package/snippets/{error-pages → twig-symfony}/bundles/TwigBundle/Exception/error.html.twig +0 -0
- /package/snippets/{flash-messages.html.twig → twig-symfony/flash-messages.html.twig} +0 -0
- /package/snippets/{minimo-form-layout.html.twig → twig-symfony/minimo-form-layout.html.twig} +0 -0
|
@@ -0,0 +1,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
|
+
}
|