@massimo-cassandro/minimo 1.33.0 → 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 (84) hide show
  1. package/README.md +2 -1
  2. package/dev-tools/starter-kit/source_files/{CLAUDE-frontend.md → app/CLAUDE.md} +1 -1
  3. package/dev-tools/starter-kit/source_files/app/assets/api_htaccess +20 -0
  4. package/dev-tools/starter-kit/source_files/{layout-critical.css → app/css/layout.critical.css} +1 -1
  5. package/dev-tools/starter-kit/source_files/app/css/layout.css +0 -0
  6. package/dev-tools/starter-kit/source_files/app/index.js +1 -0
  7. package/dev-tools/starter-kit/source_files/root/webpack-config-modules/inline-critical-css.mjs +41 -0
  8. package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-setup.mjs +1 -1
  9. package/dev-tools/starter-kit/{webpack.config.mjs → source_files/root/webpack.config.mjs} +50 -24
  10. package/dev-tools/starter-kit/starter-install.sh +141 -83
  11. package/dev-tools/stylelint-config/test/test.module.scss +9 -0
  12. package/package.json +1 -1
  13. package/snippets/php/iviewer.php +488 -0
  14. package/snippets/twig-symfony/ViewerController.php +549 -0
  15. package/src/custom-properties.css +29 -0
  16. package/src/minimo.css +3 -0
  17. package/src/utilities/dom-builder/dom-builder.js +9 -5
  18. package/src/web-components/json-table/json-table-component.js +387 -38
  19. package/src/web-components/json-table/json-table-component.module.css +190 -0
  20. package/src/web-components/json-table/json-table.minimo.tokens.mjs +195 -0
  21. package/src/web-components/json-table/readme.md +603 -82
  22. package/src/web-components/json-table/src/cell-content.js +136 -0
  23. package/src/web-components/json-table/src/content-utils.js +103 -0
  24. package/src/web-components/json-table/src/data-types.js +280 -0
  25. package/src/web-components/json-table/src/defaults.js +377 -58
  26. package/src/web-components/json-table/src/get-data.js +105 -10
  27. package/src/web-components/json-table/src/icons.js +80 -0
  28. package/src/web-components/json-table/src/info-section.js +85 -50
  29. package/src/web-components/json-table/src/main-builder.js +175 -52
  30. package/src/web-components/json-table/src/pagination.js +253 -0
  31. package/src/web-components/json-table/src/parse-cols.js +113 -0
  32. package/src/web-components/json-table/src/parse-rows.js +81 -0
  33. package/src/web-components/json-table/src/resolve-params.js +102 -32
  34. package/src/web-components/json-table/src/search.js +71 -0
  35. package/src/web-components/json-table/src/sorting.js +111 -0
  36. package/src/web-components/json-table/src/table-body.js +109 -0
  37. package/src/web-components/json-table/src/table-builder.js +152 -0
  38. package/src/web-components/json-table/src/table-tfoot.js +56 -0
  39. package/src/web-components/json-table/src/table-thead.js +183 -0
  40. package/src/web-components/json-table/src/update-info.js +43 -24
  41. package/src/web-components/s-datatable-component/{docs.md → readme.md} +5 -3
  42. package/src/web-components/s-datatable-component/s-datatable-component.js +1 -1
  43. package/types/global.d.ts +17 -3
  44. package/dev-tools/starter-kit/source_files/app_index.js +0 -1
  45. package/snippets/config-templates/_htaccess +0 -69
  46. package/snippets/config-templates/robots.txt +0 -5
  47. package/snippets/config-templates/rollup.config.mjs +0 -115
  48. /package/dev-tools/starter-kit/source_files/{_root_robots.txt → app/assets/robots.txt} +0 -0
  49. /package/dev-tools/starter-kit/source_files/{_root_htaccess → app/assets/root_htaccess} +0 -0
  50. /package/{src → dev-tools/starter-kit/source_files/app/css}/fonts.css +0 -0
  51. /package/dev-tools/starter-kit/source_files/{favicons-readme.md → app/favicons/favicons-readme.md} +0 -0
  52. /package/dev-tools/starter-kit/{webpack-template.ejs → source_files/app/tpl/index.ejs} +0 -0
  53. /package/dev-tools/starter-kit/source_files/{__project__.code-workspace → root/__project__.code-workspace} +0 -0
  54. /package/dev-tools/starter-kit/source_files/{_browserslistrc → root/_browserslistrc} +0 -0
  55. /package/dev-tools/starter-kit/source_files/{_editorconfig → root/_editorconfig} +0 -0
  56. /package/dev-tools/starter-kit/source_files/{_gitignore → root/_gitignore} +0 -0
  57. /package/dev-tools/starter-kit/source_files/{_prettierrc → root/_prettierrc} +0 -0
  58. /package/dev-tools/starter-kit/source_files/{eslint.config.mjs → root/eslint.config.mjs} +0 -0
  59. /package/dev-tools/starter-kit/source_files/{jsconfig.json → root/jsconfig.json} +0 -0
  60. /package/dev-tools/starter-kit/{package-tpl.json → source_files/root/package-tpl.json} +0 -0
  61. /package/dev-tools/starter-kit/source_files/{stylelint.config.mjs → root/stylelint.config.mjs} +0 -0
  62. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/css-rules.mjs +0 -0
  63. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/get-jsConfig-aliases.mjs +0 -0
  64. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/mini-svg-data-uri-loader.cjs +0 -0
  65. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/postcss.config.mjs +0 -0
  66. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/purgecss-variables-safelist.mjs +0 -0
  67. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svg-rules.mjs +0 -0
  68. /package/dev-tools/starter-kit/{webpack-config-modules → source_files/root/webpack-config-modules}/svgo.config.mjs +0 -0
  69. /package/snippets/{config-templates/_github → _github}/dependabot.yml +0 -0
  70. /package/snippets/{error-pages/error-pages.js → error-pages.js} +0 -0
  71. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.jsx +0 -0
  72. /package/snippets/{config-templates/react → react}/offcanvas/offcanvas.module.scss +0 -0
  73. /package/snippets/{config-templates → root}/_hintrc.cjs +0 -0
  74. /package/snippets/{config-templates → root}/_npmignore +0 -0
  75. /package/snippets/{config-templates → root}/_npmrc +0 -0
  76. /package/snippets/{config-templates → root}/_stylelintignore +0 -0
  77. /package/snippets/{config-templates → root}/babel.config--react.json +0 -0
  78. /package/snippets/{config-templates → root}/babel.config--solidJS.json +0 -0
  79. /package/snippets/{ImgViewerController.php → twig-symfony/ImgViewerController.php} +0 -0
  80. /package/snippets/{UnsplashController.php → twig-symfony/UnsplashController.php} +0 -0
  81. /package/snippets/{config-templates → twig-symfony}/_env.local +0 -0
  82. /package/snippets/{error-pages → twig-symfony}/bundles/TwigBundle/Exception/error.html.twig +0 -0
  83. /package/snippets/{flash-messages.html.twig → twig-symfony/flash-messages.html.twig} +0 -0
  84. /package/snippets/{minimo-form-layout.html.twig → twig-symfony/minimo-form-layout.html.twig} +0 -0
@@ -0,0 +1,80 @@
1
+ /*! minimo - json-table: icons */
2
+
3
+ import checkBold from '../../../icons/check-bold.svg?inline';
4
+ import xBold from '../../../icons/x-bold.svg?inline';
5
+ import arrowUp from '../../../icons/arrow-up.svg?inline';
6
+ import arrowDown from '../../../icons/arrow-down.svg?inline';
7
+ import arrowsDownUp from '../../../icons/arrows-down-up.svg?inline';
8
+ import caretLeft from '../../../icons/caret-left.svg?inline';
9
+ import caretRight from '../../../icons/caret-right.svg?inline';
10
+
11
+ /** @typedef {import('./defaults.js').IconDef} IconDef */
12
+
13
+ /** Default icon of `true` values (minimo `check-bold`, inline SVG markup) */
14
+ export const boolTrueIcon = checkBold;
15
+ /** Default icon of `false` values (minimo `x-bold`) */
16
+ export const boolFalseIcon = xBold;
17
+ /** Default sort button icon, ascending sort active (minimo `arrow-up`) */
18
+ export const sortAscArrowIcon = arrowUp;
19
+ /** Default sort button icon, descending sort active (minimo `arrow-down`) */
20
+ export const sortDescArrowIcon = arrowDown;
21
+ /** Default sort button icon, no sort active (minimo `arrows-down-up`) */
22
+ export const sortNoneArrowIcon = arrowsDownUp;
23
+ /** Default previous page button icon (minimo `caret-left`) */
24
+ export const paginationPrevIcon = caretLeft;
25
+ /** Default next page button icon (minimo `caret-right`) */
26
+ export const paginationNextIcon = caretRight;
27
+
28
+ /**
29
+ * Markup strings already parsed, keyed by the string itself.
30
+ * @type {Map<string, DocumentFragment>}
31
+ */
32
+ const fragmentCache = new Map();
33
+
34
+ /**
35
+ * Resolves an icon definition to a content usable by domBuilder (`content`) or `setContent()`.
36
+ *
37
+ * Markup strings are parsed once through an inert `<template>` and cloned on every call, so the
38
+ * same icon can be inserted in many cells without re-parsing and without going through the HTML
39
+ * sanitizer used by domBuilder for markup strings (icons are developer-supplied configuration,
40
+ * not user data). DOM nodes are cloned too, since a node can live in one place only.
41
+ *
42
+ * @param {IconDef|null|undefined} icon - Icon definition (see `IconDef`)
43
+ * @returns {Node|DomBuilderItem[]|null} A fresh node (or domBuilder array), `null` when `icon` is nullish
44
+ *
45
+ * @example
46
+ * iconContent('<svg …></svg>'); // → DocumentFragment (clone of the parsed markup)
47
+ * iconContent(document.querySelector('svg')); // → cloned SVGElement
48
+ * iconContent({ tag: 'span', content: '✓' }); // → [{ tag: 'span', content: '✓' }] (domBuilder array)
49
+ * iconContent(() => '<svg …></svg>'); // → DocumentFragment
50
+ * iconContent(null); // → null
51
+ */
52
+ export function iconContent(icon) {
53
+
54
+ const resolved = typeof icon === 'function' ? icon() : icon;
55
+
56
+ if (resolved == null) {
57
+ return null;
58
+ }
59
+
60
+ if (resolved instanceof Node) {
61
+ return resolved.cloneNode(true);
62
+ }
63
+
64
+ if (typeof resolved === 'string') {
65
+ let fragment = fragmentCache.get(resolved);
66
+ if (!fragment) {
67
+ const tpl = document.createElement('template');
68
+ tpl.innerHTML = resolved.trim();
69
+ fragment = tpl.content;
70
+ fragmentCache.set(resolved, fragment);
71
+ }
72
+ return fragment.cloneNode(true);
73
+ }
74
+
75
+ if (typeof resolved === 'object') {
76
+ return [resolved];
77
+ }
78
+
79
+ return null;
80
+ }
@@ -1,4 +1,4 @@
1
- /*! minimo - json-table: info section */
1
+ /*! minimo - json-table: info section parts */
2
2
 
3
3
  import * as styles from '../json-table-component.module.css';
4
4
  import { classnames } from '../../../utilities/classnames.js';
@@ -7,66 +7,101 @@ import { classnames } from '../../../utilities/classnames.js';
7
7
  /** @typedef {import('./main-builder.js').JsonTableElements} JsonTableElements */
8
8
 
9
9
  /**
10
- * Builds the domBuilder configuration of the info section (info text + search input).
10
+ * domBuilder item of the info text container (template slot `resultInfo`).
11
+ *
12
+ * Generated structure: `div.resultInfo[classes.resultInfo][aria-live=polite]`; the content is
13
+ * set by `update-info.js`. The element is stored in `elements.resultInfo`.
14
+ *
15
+ * @param {JsonTableParams} params - Resolved params
16
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
17
+ * @returns {DomBuilderItem}
18
+ *
19
+ * @example
20
+ * domBuilder([resultInfoPart(params, elements)], host);
21
+ * elements.resultInfo; // → HTMLElement
22
+ */
23
+ export function resultInfoPart(params, elements) {
24
+ return {
25
+ className: classnames(styles.resultInfo, params.classes.resultInfo),
26
+ attrs: { 'aria-live': 'polite' },
27
+ callback: el => { elements.resultInfo = /** @type {HTMLElement} */ (el); }
28
+ };
29
+ }
30
+
31
+
32
+ /**
33
+ * domBuilder item of the search input (template slot `search`), rendered only when
34
+ * `params.search` is true.
35
+ *
36
+ * Generated structure: `div.search[classes.search] > input[type=search][classes.searchInput]`.
37
+ * The input is stored in `elements.searchInput`.
38
+ *
39
+ * TODO listener sull'input di ricerca: step 3
40
+ *
41
+ * @param {JsonTableParams} params - Resolved params
42
+ * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
43
+ * @returns {DomBuilderItem}
44
+ *
45
+ * @example
46
+ * domBuilder([searchPart(params, elements)], host);
47
+ * elements.searchInput; // → HTMLInputElement (when params.search is true)
48
+ */
49
+ export function searchPart(params, elements) {
50
+ return {
51
+ condition: params.search,
52
+ className: classnames(styles.search, params.classes.search),
53
+ children: [
54
+ {
55
+ tag: 'input',
56
+ className: classnames(params.classes.searchInput),
57
+ attrs: {
58
+ type: 'search',
59
+ title: params.labels.searchTitle,
60
+ placeholder: params.labels.searchPlaceholder,
61
+ 'aria-label': params.labels.searchAriaLabel
62
+ },
63
+ callback: el => { elements.searchInput = /** @type {HTMLInputElement} */ (el); }
64
+ }
65
+ ]
66
+ };
67
+ }
68
+
69
+
70
+ /**
71
+ * domBuilder item of the whole info section (template slot `infoSection`): info text + search input.
11
72
  *
12
73
  * Generated structure:
13
74
  * ```
14
- * div.infoOuter[outerInfoExtraClass]
15
- * div.info[infoExtraClass]
16
- * div.resultInfo ← info text (see `update-info.js`)
17
- * div.search > input[type=search] ← only if `params.search` is true
75
+ * div.infoOuter[classes.infoOuter]
76
+ * div.info[classes.info]
77
+ * div.resultInfo ← resultInfoPart()
78
+ * div.search > input[type=search] ← searchPart(), only if `params.search` is true
18
79
  * ```
19
80
  *
20
81
  * References to the generated elements are stored in `elements`
21
82
  * (`infoOuter`, `info`, `resultInfo`, `searchInput`).
22
83
  *
23
- * TODO listener sull'input di ricerca: da impostare quando verrà implementata la ricerca
24
- *
25
84
  * @param {JsonTableParams} params - Resolved params
26
85
  * @param {JsonTableElements} elements - Object collecting the generated elements (mutated)
27
- * @returns {DomBuilderItem[]} domBuilder items to be used as children of the main wrapper
86
+ * @returns {DomBuilderItem}
28
87
  *
29
88
  * @example
30
- * const elements = {};
31
- * domBuilder([{ tag: 'section', children: [...infoSection(params, elements)] }], host);
32
- * elements.searchInput; // → HTMLInputElement (when params.search is true)
89
+ * domBuilder([{ tag: 'section', children: [infoSectionPart(params, elements)] }], host);
90
+ * elements.info; // → HTMLElement
33
91
  */
34
- export function infoSection(params, elements) {
35
-
36
- return [
37
- {
38
- className: classnames(styles.infoOuter, params.outerInfoExtraClass),
39
- callback: el => { elements.infoOuter = /** @type {HTMLElement} */ (el); },
40
- children: [
41
- {
42
- className: classnames(styles.info, params.infoExtraClass),
43
- callback: el => { elements.info = /** @type {HTMLElement} */ (el); },
44
- children: [
45
- {
46
- className: styles.resultInfo,
47
- attrs: { 'aria-live': 'polite' },
48
- callback: el => { elements.resultInfo = /** @type {HTMLElement} */ (el); }
49
- },
50
- {
51
- condition: params.search,
52
- className: styles.search,
53
- children: [
54
- {
55
- tag: 'input',
56
- className: classnames(params.searchInputClass),
57
- attrs: {
58
- type: 'search',
59
- title: params.searchInputTitle,
60
- placeholder: params.searchInputPlaceholder,
61
- 'aria-label': params.searchInputAriaLabel
62
- },
63
- callback: el => { elements.searchInput = /** @type {HTMLInputElement} */ (el); }
64
- }
65
- ]
66
- }
67
- ]
68
- }
69
- ]
70
- }
71
- ];
92
+ export function infoSectionPart(params, elements) {
93
+ return {
94
+ className: classnames(styles.infoOuter, params.classes.infoOuter),
95
+ callback: el => { elements.infoOuter = /** @type {HTMLElement} */ (el); },
96
+ children: [
97
+ {
98
+ className: classnames(styles.info, params.classes.info),
99
+ callback: el => { elements.info = /** @type {HTMLElement} */ (el); },
100
+ children: [
101
+ resultInfoPart(params, elements),
102
+ searchPart(params, elements)
103
+ ]
104
+ }
105
+ ]
106
+ };
72
107
  }
@@ -3,93 +3,216 @@
3
3
  import * as styles from '../json-table-component.module.css';
4
4
  import { domBuilder } from '../../../utilities/dom-builder/dom-builder.js';
5
5
  import { classnames } from '../../../utilities/classnames.js';
6
- import { infoSection } from './info-section.js';
6
+ import { defaults } from './defaults.js';
7
+ import { infoSectionPart, resultInfoPart, searchPart } from './info-section.js';
8
+ import { tablePart, tableFooterPart, captionPart, captionId } from './table-builder.js';
9
+ import { paginationPart } from './pagination.js';
7
10
 
8
- /** @typedef {import('./defaults.js').JsonTableParams} JsonTableParams */
11
+ /** @typedef {import('../json-table-component.js').JsonTable} JsonTable */
12
+ /** @typedef {import('./defaults.js').TemplateItem} TemplateItem */
13
+ /** @typedef {import('./defaults.js').TemplateSlot} TemplateSlot */
9
14
 
10
15
  /**
11
16
  * Elements generated by the component, collected during the build.
12
17
  * @typedef {Object} JsonTableElements
13
- * @property {HTMLElement} [wrapper] - Main wrapper (`section`), contains info section and table
18
+ * @property {HTMLElement} [wrapper] - Main wrapper (`section`), contains the template parts
14
19
  * @property {HTMLElement} [infoOuter] - Outer info container
15
20
  * @property {HTMLElement} [info] - Info container (flex row: info text + search)
16
21
  * @property {HTMLElement} [resultInfo] - Info text container
17
22
  * @property {HTMLInputElement} [searchInput] - Search input (only when `search` is true)
18
23
  * @property {HTMLElement} [tableWrapper] - Div wrapping the table (`table-responsive` by default)
19
24
  * @property {HTMLTableElement} [table] - The `<table>` element
25
+ * @property {HTMLTableSectionElement} [thead] - The `<thead>` element
26
+ * @property {HTMLTableSectionElement} [tbody] - The `<tbody>` element
27
+ * @property {HTMLTableSectionElement} [tfoot] - The `<tfoot>` element (only when `tfoot` is true)
28
+ * @property {HTMLElement} [tableFooter] - Footer bar below the table (caption + pagination), when rendered
29
+ * @property {HTMLElement} [caption] - Caption container (only when `caption` is set)
30
+ * @property {HTMLElement} [pagination] - Pagination `<nav>` (only when `perPage` > 0)
20
31
  */
21
32
 
22
33
  /**
23
- * Builds the whole component structure inside `host` (emptied first) via domBuilder.
34
+ * Whether a template item is a slot placeholder.
35
+ * @param {*} item
36
+ * @returns {item is { slot: TemplateSlot }}
37
+ */
38
+ function isSlot(item) {
39
+ return item != null && typeof item === 'object' && !(item instanceof Node) && typeof item.slot === 'string';
40
+ }
41
+
42
+
43
+ /**
44
+ * Collects, recursively (`children` and array `content`), the names of the slots used by a template.
45
+ *
46
+ * @param {Array<TemplateItem|string|Node>} items - Template items
47
+ * @param {Set<string>} [found=new Set()] - Accumulator (default: new Set())
48
+ * @returns {Set<string>}
24
49
  *
25
- * Generated structure:
50
+ * @example
51
+ * collectSlots([{ slot: 'table' }, { children: [{ slot: 'pagination' }] }]); // → Set { 'table', 'pagination' }
52
+ */
53
+ export function collectSlots(items, found = new Set()) {
54
+
55
+ items.forEach(item => {
56
+ if (isSlot(item)) {
57
+ found.add(item.slot);
58
+ } else if (item != null && typeof item === 'object' && !(item instanceof Node)) {
59
+ if (Array.isArray(item.children)) {
60
+ collectSlots(item.children, found);
61
+ }
62
+ if (Array.isArray(item.content)) {
63
+ collectSlots(/** @type {TemplateItem[]} */ (item.content), found);
64
+ }
65
+ }
66
+ });
67
+
68
+ return found;
69
+ }
70
+
71
+
72
+ /**
73
+ * Replaces, recursively (`children` and array `content`), every `{ slot: 'name' }` item of a
74
+ * template with the corresponding built-in part. Unknown slots are reported in the console
75
+ * and skipped. Non-slot items are shallow-copied, so the original template is never mutated.
76
+ *
77
+ * @param {Array<TemplateItem|string|Node>} items - Template items
78
+ * @param {Object<string, DomBuilderItem>} parts - Built-in parts, keyed by slot name
79
+ * @returns {Array<DomBuilderItem|string|Node>}
80
+ *
81
+ * @example
82
+ * expandSlots([{ className: 'row', children: [{ slot: 'search' }, { slot: 'resultInfo' }] }, { slot: 'table' }], parts);
83
+ * // → [{ className: 'row', children: [<search part>, <resultInfo part>] }, <table part>]
84
+ */
85
+ export function expandSlots(items, parts) {
86
+
87
+ /** @type {Array<DomBuilderItem|string|Node>} */
88
+ const result = [];
89
+
90
+ items.forEach(item => {
91
+
92
+ if (item == null || typeof item !== 'object' || item instanceof Node) {
93
+ result.push(/** @type {string|Node} */ (item));
94
+ return;
95
+ }
96
+
97
+ if (isSlot(item)) {
98
+ const part = parts[item.slot];
99
+ if (!part) {
100
+ // eslint-disable-next-line no-console
101
+ console.error(`[json-table] template: slot \`${item.slot}\` non riconosciuto (disponibili: ${Object.keys(parts).join(', ')})`);
102
+ return;
103
+ }
104
+ result.push(part);
105
+ return;
106
+ }
107
+
108
+ /** @type {DomBuilderItem} */
109
+ const copy = /** @type {DomBuilderItem} */ ({ ...item });
110
+
111
+ if (Array.isArray(item.children)) {
112
+ copy.children = expandSlots(item.children, parts);
113
+ }
114
+ if (Array.isArray(item.content)) {
115
+ copy.content = /** @type {DomBuilderItem[]} */ (expandSlots(/** @type {TemplateItem[]} */ (item.content), parts));
116
+ }
117
+
118
+ result.push(copy);
119
+ });
120
+
121
+ return result;
122
+ }
123
+
124
+
125
+ /**
126
+ * Builds the whole component structure inside the host element (emptied first) via domBuilder:
127
+ * a fixed main wrapper whose content is defined by `params.template`.
128
+ *
129
+ * Available template slots (see `TemplateItem`):
130
+ * - `infoSection` → `infoSectionPart()` (info text + search input)
131
+ * - `resultInfo` → `resultInfoPart()` (info text only)
132
+ * - `search` → `searchPart()` (search input only)
133
+ * - `table` → `tablePart()` (table wrapper + table with thead, tbody, tfoot, plus the footer bar)
134
+ * - `caption` → `captionPart()` (caption container; part of the `table` footer bar unless placed explicitly)
135
+ * - `pagination` → `paginationPart()` (pagination nav; idem)
136
+ *
137
+ * Generated structure with the default template (`[{ slot: 'infoSection' }, { slot: 'table' }]`):
26
138
  * ```
27
- * section.wrapper[mainWrapperExtraClass]
28
- * div.infoOuter[outerInfoExtraClass]
29
- * div.info[infoExtraClass]
139
+ * section.wrapper[classes.wrapper]
140
+ * div.infoOuter[classes.infoOuter]
141
+ * div.info[classes.info]
30
142
  * div.resultInfo
31
143
  * div.search > input[type=search] (if `search`)
32
- * div[tableWrapperClass]
33
- * table#tableId[tableClass]
34
- * caption (if `caption`)
144
+ * div.tableOuter
145
+ * div.tableWrapper[classes.tableWrapper]
146
+ * table#tableId.table[classes.table]
147
+ * thead / tbody / tfoot (tfoot if `tfoot`)
148
+ * div.tableFooter[classes.tableFooter] (if `caption` or `perPage` > 0)
149
+ * div.caption (if `caption`)
150
+ * nav.pagination (if `perPage` > 0)
35
151
  * ```
36
152
  *
37
- * TODO thead / tbody / tfoot: step successivo (parametro `cols` e rendering delle righe)
153
+ * The `<tbody>`, `<tfoot>` and pagination are created empty: see `renderTbody()`, `renderTfoot()`
154
+ * and `renderPagination()`.
38
155
  *
39
- * @param {HTMLElement} host - The `<json-table>` element
40
- * @param {JsonTableParams} params - Resolved params
156
+ * @param {JsonTable} jt - The component instance (host element, `params`, `cols`)
41
157
  * @returns {JsonTableElements} References to the generated elements
42
158
  *
43
159
  * @example
44
- * const elements = mainBuilder(el, {
45
- * ...defaults,
46
- * caption: 'Utenti', // default: null
47
- * search: true, // default: true
48
- * tableId: 'users-table', // default: null
49
- * tableWrapperClass: 'table-responsive', // default: 'table-responsive'
50
- * tableClass: 'table table-bordered', // default: 'table table-bordered'
51
- * mainWrapperExtraClass: null, // default: null
52
- * outerInfoExtraClass: null, // default: null
53
- * infoExtraClass: null // default: null
54
- * });
160
+ * jt.params.template = [
161
+ * { className: 'flex gap-2', children: [{ slot: 'search' }, { slot: 'pagination' }] },
162
+ * { slot: 'table' }, // footer bar with the caption only: the pagination is placed above
163
+ * { slot: 'resultInfo' }
164
+ * ];
165
+ * const elements = mainBuilder(jt);
55
166
  * elements.table; // → HTMLTableElement
56
167
  */
57
- export function mainBuilder(host, params) {
168
+ export function mainBuilder(jt) {
169
+
170
+ const { params } = jt;
58
171
 
59
172
  /** @type {JsonTableElements} */
60
173
  const elements = {};
61
174
 
175
+ const capId = captionId(params);
176
+ const captionItem = captionPart(jt, elements, capId);
177
+ const paginationItem = paginationPart(jt, elements);
178
+
179
+ // footer bar parts: removed below when the template places them elsewhere
180
+ /** @type {DomBuilderItem[]} */
181
+ const footerChildren = [captionItem, paginationItem];
182
+ const footerBar = tableFooterPart(jt, elements, footerChildren);
183
+
184
+ /** @type {Object<string, DomBuilderItem>} */
185
+ const parts = {
186
+ infoSection: infoSectionPart(params, elements),
187
+ resultInfo: resultInfoPart(params, elements),
188
+ search: searchPart(params, elements),
189
+ caption: captionItem,
190
+ pagination: paginationItem,
191
+ table: tablePart(jt, elements, footerBar, capId)
192
+ };
193
+
194
+ const template = typeof params.template === 'function'
195
+ ? params.template(parts, params)
196
+ : params.template;
197
+
198
+ const items = Array.isArray(template) ? template : /** @type {TemplateItem[]} */ (defaults.template);
199
+ const usedSlots = collectSlots(items);
200
+
201
+ ['caption', 'pagination'].forEach(slot => {
202
+ if (usedSlots.has(slot)) {
203
+ footerChildren.splice(footerChildren.indexOf(parts[slot]), 1);
204
+ }
205
+ });
206
+ footerBar.condition = footerChildren.some(item => item.condition !== false);
207
+
62
208
  domBuilder([
63
209
  {
64
210
  tag: 'section',
65
- className: classnames(styles.wrapper, params.mainWrapperExtraClass),
211
+ className: classnames(styles.wrapper, params.classes.wrapper),
66
212
  callback: el => { elements.wrapper = /** @type {HTMLElement} */ (el); },
67
- children: [
68
-
69
- ...infoSection(params, elements),
70
-
71
- {
72
- className: classnames(params.tableWrapperClass),
73
- callback: el => { elements.tableWrapper = /** @type {HTMLElement} */ (el); },
74
- children: [
75
- {
76
- tag: 'table',
77
- id: params.tableId,
78
- className: classnames(styles.table, params.tableClass),
79
- callback: el => { elements.table = /** @type {HTMLTableElement} */ (el); },
80
- children: [
81
- {
82
- tag: 'caption',
83
- condition: params.caption != null,
84
- content: params.caption
85
- }
86
- ]
87
- }
88
- ]
89
- }
90
- ]
213
+ children: expandSlots(items, parts)
91
214
  }
92
- ], host, { emptyParent: true });
215
+ ], jt, { emptyParent: true });
93
216
 
94
217
  return elements;
95
218
  }