@appius-fr/apx 2.6.1 → 2.7.0
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/APX.mjs +2 -0
- package/README.md +207 -203
- package/dist/2ab50e700c8fbddb45e0.svg +10 -0
- package/dist/2e967d8dd752e0bed703.svg +10 -0
- package/dist/5ddaeefe5dfbc8e09652.svg +7 -0
- package/dist/6dc2907ba3bbb232601d.svg +10 -0
- package/dist/6e1e61dfca176a885b8d.svg +3 -0
- package/dist/6f3a0a27a260bb2c221b.svg +9 -0
- package/dist/8b07a8bf719a38262b7d.svg +10 -0
- package/dist/APX.dev.mjs +843 -245
- package/dist/APX.mjs +1 -1
- package/dist/APX.prod.mjs +1 -1
- package/dist/APX.standalone.js +744 -92
- package/dist/APX.standalone.js.map +1 -1
- package/dist/bdfa755a1cdb872368c7.svg +3 -0
- package/dist/c9da177f7663f9fcd023.svg +10 -0
- package/dist/ce9ef5fceb78e17e68c9.svg +8 -0
- package/dist/ed5af5163957b04bc6cc.svg +7 -0
- package/modules/listen/README.md +242 -235
- package/modules/listen/listen.mjs +1 -3
- package/modules/scrollableTable/README.md +52 -0
- package/modules/scrollableTable/css/scrollableTable.css +60 -0
- package/modules/scrollableTable/scrollableTable.mjs +198 -0
- package/modules/toast/README.md +186 -153
- package/modules/tools/form-packer/README.md +12 -14
- package/modules/tools/form-packer/augment-apx.mjs +4 -2
- package/modules/tools/form-packer/packToJson.mjs +58 -16
- package/modules/tristate/CHANGELOG.md +34 -0
- package/modules/tristate/README.md +157 -94
- package/modules/tristate/assets/tristate-checked.svg +3 -0
- package/modules/tristate/assets/tristate-cross.svg +10 -0
- package/modules/tristate/assets/tristate-crossed.svg +3 -0
- package/modules/tristate/assets/tristate-indeterminate-dash.svg +9 -0
- package/modules/tristate/assets/tristate-tick.svg +10 -0
- package/modules/tristate/assets/tristate-unchecked.svg +7 -0
- package/modules/tristate/css/tristate.css +91 -24
- package/modules/tristate/tristate.mjs +292 -171
- package/package.json +5 -1
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import './css/scrollableTable.css';
|
|
2
|
+
|
|
3
|
+
const DATA_KEY = '_apxScrollableTable';
|
|
4
|
+
const CLASS_TABLE = 'apx-scrollable-table';
|
|
5
|
+
|
|
6
|
+
const DEFAULT_MAX_HEIGHT = '200px';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get number of columns from the first row that has cells (sum of colspan).
|
|
10
|
+
* @param {HTMLTableSectionElement} section - thead, tbody, or tfoot
|
|
11
|
+
* @returns {number}
|
|
12
|
+
*/
|
|
13
|
+
function getColumnCountFromSection(section) {
|
|
14
|
+
const firstRow = section?.querySelector(':scope > tr');
|
|
15
|
+
if (!firstRow) return 0;
|
|
16
|
+
let cols = 0;
|
|
17
|
+
firstRow.querySelectorAll(':scope > th, :scope > td').forEach((cell) => {
|
|
18
|
+
cols += parseInt(cell.getAttribute('colspan'), 10) || 1;
|
|
19
|
+
});
|
|
20
|
+
return cols;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get total column count for the table (from thead or tbody first row).
|
|
25
|
+
* @param {HTMLTableElement} table
|
|
26
|
+
* @returns {number}
|
|
27
|
+
*/
|
|
28
|
+
function getTableColumnCount(table) {
|
|
29
|
+
const thead = table.querySelector('thead');
|
|
30
|
+
const tbody = table.querySelector('tbody');
|
|
31
|
+
const countFromThead = thead ? getColumnCountFromSection(thead) : 0;
|
|
32
|
+
const countFromTbody = tbody ? getColumnCountFromSection(tbody) : 0;
|
|
33
|
+
return countFromThead || countFromTbody || 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Count direct tr children in a section.
|
|
38
|
+
* @param {HTMLTableSectionElement} section
|
|
39
|
+
* @returns {number}
|
|
40
|
+
*/
|
|
41
|
+
function getRowCount(section) {
|
|
42
|
+
if (!section) return 0;
|
|
43
|
+
return section.querySelectorAll(':scope > tr').length;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Build a 2D grid of occupied slots for a section (for rowspan/colspan placement).
|
|
48
|
+
* Place each cell in DOM order; return a list of { cell, row, col, colspan, rowspan }.
|
|
49
|
+
* @param {HTMLTableSectionElement} section
|
|
50
|
+
* @param {number} numRows
|
|
51
|
+
* @param {number} numCols
|
|
52
|
+
* @returns {{ cell: HTMLTableCellElement, row: number, col: number, colspan: number, rowspan: number }[]}
|
|
53
|
+
*/
|
|
54
|
+
function computeCellPlacements(section, numRows, numCols) {
|
|
55
|
+
if (!section || numRows === 0 || numCols === 0) return [];
|
|
56
|
+
const occupied = Array.from({ length: numRows }, () => Array(numCols).fill(false));
|
|
57
|
+
const placements = [];
|
|
58
|
+
const rows = section.querySelectorAll(':scope > tr');
|
|
59
|
+
|
|
60
|
+
for (let r = 0; r < rows.length; r++) {
|
|
61
|
+
const tr = rows[r];
|
|
62
|
+
const cells = tr.querySelectorAll(':scope > th, :scope > td');
|
|
63
|
+
for (const cell of cells) {
|
|
64
|
+
const colspan = Math.min(parseInt(cell.getAttribute('colspan'), 10) || 1, numCols);
|
|
65
|
+
const rowspan = Math.min(parseInt(cell.getAttribute('rowspan'), 10) || 1, numRows - r);
|
|
66
|
+
let col = 0;
|
|
67
|
+
while (col < numCols) {
|
|
68
|
+
let free = true;
|
|
69
|
+
for (let rr = r; rr < r + rowspan && free; rr++) {
|
|
70
|
+
for (let cc = col; cc < col + colspan && free; cc++) {
|
|
71
|
+
if (occupied[rr]?.[cc]) free = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (free) break;
|
|
75
|
+
col++;
|
|
76
|
+
}
|
|
77
|
+
if (col + colspan > numCols) continue;
|
|
78
|
+
for (let rr = r; rr < r + rowspan; rr++) {
|
|
79
|
+
for (let cc = col; cc < col + colspan; cc++) {
|
|
80
|
+
if (occupied[rr]) occupied[rr][cc] = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
placements.push({ cell, row: r, col, colspan, rowspan });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return placements;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Apply grid placement styles to a list of placements (1-based line numbers for CSS Grid).
|
|
91
|
+
*/
|
|
92
|
+
function applyPlacements(placements) {
|
|
93
|
+
placements.forEach(({ cell, row, col, colspan, rowspan }) => {
|
|
94
|
+
cell.style.gridRow = `${row + 1} / span ${rowspan}`;
|
|
95
|
+
cell.style.gridColumn = `${col + 1} / span ${colspan}`;
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Clear grid placement styles from all th/td in a table (for refresh).
|
|
101
|
+
* @param {HTMLTableElement} table
|
|
102
|
+
*/
|
|
103
|
+
function clearPlacements(table) {
|
|
104
|
+
table.querySelectorAll('th, td').forEach((cell) => {
|
|
105
|
+
cell.style.gridRow = '';
|
|
106
|
+
cell.style.gridColumn = '';
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Normalize maxHeight option to a CSS length string.
|
|
112
|
+
* @param {number|string} value
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function normalizeMaxHeight(value) {
|
|
116
|
+
if (value == null) return DEFAULT_MAX_HEIGHT;
|
|
117
|
+
if (typeof value === 'number') return `${value}px`;
|
|
118
|
+
return String(value);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Apply scrollable table layout to a single table.
|
|
123
|
+
* @param {HTMLTableElement} table
|
|
124
|
+
* @param {{ maxHeight?: number|string }} options
|
|
125
|
+
*/
|
|
126
|
+
function applyScrollableTable(table, options) {
|
|
127
|
+
const thead = table.querySelector('thead');
|
|
128
|
+
const tbody = table.querySelector('tbody');
|
|
129
|
+
const tfoot = table.querySelector('tfoot');
|
|
130
|
+
|
|
131
|
+
const numCols = getTableColumnCount(table);
|
|
132
|
+
if (numCols === 0) return;
|
|
133
|
+
|
|
134
|
+
const theadRows = getRowCount(thead);
|
|
135
|
+
const tbodyRows = getRowCount(tbody);
|
|
136
|
+
const tfootRows = getRowCount(tfoot);
|
|
137
|
+
|
|
138
|
+
table.style.setProperty('--apx-scrollable-cols', String(numCols));
|
|
139
|
+
table.style.setProperty('--apx-scrollable-thead-rows', String(Math.max(1, theadRows)));
|
|
140
|
+
table.style.setProperty('--apx-scrollable-tbody-rows', String(Math.max(1, tbodyRows)));
|
|
141
|
+
table.style.setProperty('--apx-scrollable-tfoot-rows', String(Math.max(1, tfootRows)));
|
|
142
|
+
table.style.setProperty('--apx-scrollable-body-max-height', normalizeMaxHeight(options.maxHeight));
|
|
143
|
+
|
|
144
|
+
table.classList.add(CLASS_TABLE);
|
|
145
|
+
table.classList.toggle('apx-scrollable-table--has-tfoot', !!(tfoot && tfootRows > 0));
|
|
146
|
+
table.classList.toggle('apx-scrollable-table--no-thead', !(thead && theadRows > 0));
|
|
147
|
+
|
|
148
|
+
clearPlacements(table);
|
|
149
|
+
|
|
150
|
+
const sections = [
|
|
151
|
+
{ section: thead, rows: Math.max(1, theadRows) },
|
|
152
|
+
{ section: tbody, rows: Math.max(1, tbodyRows) },
|
|
153
|
+
{ section: tfoot, rows: Math.max(1, tfootRows) }
|
|
154
|
+
];
|
|
155
|
+
sections.forEach(({ section, rows }) => {
|
|
156
|
+
if (!section) return;
|
|
157
|
+
const placements = computeCellPlacements(section, rows, numCols);
|
|
158
|
+
applyPlacements(placements);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Augments the APX object with scrollableTable(options | 'refresh').
|
|
164
|
+
* Makes the tbody of a table scrollable while keeping thead/tfoot fixed and columns aligned (CSS Grid + subgrid).
|
|
165
|
+
*
|
|
166
|
+
* @param {Object} apx - The APX object to augment.
|
|
167
|
+
* @example
|
|
168
|
+
* APX('table.data-grid').scrollableTable({ maxHeight: 300 });
|
|
169
|
+
* APX('table.data-grid').scrollableTable('refresh');
|
|
170
|
+
*/
|
|
171
|
+
export default function augmentWithScrollableTable(apx) {
|
|
172
|
+
apx.scrollableTable = function (optionsOrAction) {
|
|
173
|
+
const isRefresh = optionsOrAction === 'refresh';
|
|
174
|
+
const options = isRefresh ? null : (optionsOrAction && typeof optionsOrAction === 'object' ? optionsOrAction : {});
|
|
175
|
+
|
|
176
|
+
apx.elements.forEach((element) => {
|
|
177
|
+
if (element.tagName !== 'TABLE') return;
|
|
178
|
+
const table = /** @type {HTMLTableElement} */ (element);
|
|
179
|
+
|
|
180
|
+
const ref = table[DATA_KEY];
|
|
181
|
+
if (ref) {
|
|
182
|
+
if (isRefresh) {
|
|
183
|
+
applyScrollableTable(table, ref.options);
|
|
184
|
+
} else if (options && Object.keys(options).length > 0) {
|
|
185
|
+
ref.options = { ...ref.options, ...options };
|
|
186
|
+
applyScrollableTable(table, ref.options);
|
|
187
|
+
}
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (isRefresh) return;
|
|
191
|
+
|
|
192
|
+
applyScrollableTable(table, options);
|
|
193
|
+
table[DATA_KEY] = { options: { ...options } };
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return apx;
|
|
197
|
+
};
|
|
198
|
+
}
|
package/modules/toast/README.md
CHANGED
|
@@ -1,153 +1,186 @@
|
|
|
1
|
-
# APX Toast
|
|
2
|
-
|
|
3
|
-
A tiny, framework‑agnostic toast library for APX. Minimal CSS, ESM‑first, no globals. DOM is only touched when you actually show a toast (SSR‑safe to import).
|
|
4
|
-
|
|
5
|
-
## Install / Import
|
|
6
|
-
|
|
7
|
-
Just import `APX` — the toast CSS is automatically loaded by `modules/toast/toast.mjs`.
|
|
8
|
-
|
|
9
|
-
```html
|
|
10
|
-
<script type="module">
|
|
11
|
-
import APX from './APX.mjs';
|
|
12
|
-
// CSS is auto‑imported; no <link> tag required.
|
|
13
|
-
// ...
|
|
14
|
-
APX.toast.success('Ready!');
|
|
15
|
-
</script>
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Quick start
|
|
19
|
-
|
|
20
|
-
```js
|
|
21
|
-
// Default manager (lazy)
|
|
22
|
-
APX.toast.success('Saved!');
|
|
23
|
-
APX.toast.warning('Heads up', { durationMs: 4000 });
|
|
24
|
-
APX.toast.danger('Something failed', { durationMs: 0 }); // sticky
|
|
25
|
-
|
|
26
|
-
// Custom toast
|
|
27
|
-
const ref = APX.toast.show({ message: 'Processing…', type: 'info', durationMs: 0 });
|
|
28
|
-
// Callable shorthand for show:
|
|
29
|
-
APX.toast({ message: 'Hello', type: 'success' });
|
|
30
|
-
ref.update({ message: 'Done', type: 'success', durationMs: 1800 });
|
|
31
|
-
ref.whenClosed().then(() => console.log('closed'));
|
|
32
|
-
|
|
33
|
-
// Configure defaults at runtime
|
|
34
|
-
APX.toast.configure({ position: 'top-right', maxToasts: 4, dedupe: true });
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Named managers (profiles)
|
|
38
|
-
|
|
39
|
-
```js
|
|
40
|
-
// Register a named manager
|
|
41
|
-
APX.toast.create('admin', { position: 'bottom-left', ariaLive: 'polite' });
|
|
42
|
-
|
|
43
|
-
// Use it later
|
|
44
|
-
APX.toast.use('admin').info('Admin ready');
|
|
45
|
-
APX.toast.use('admin').closeAll();
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
## API overview
|
|
49
|
-
|
|
50
|
-
- Top‑level (default manager):
|
|
51
|
-
- `APX.toast
|
|
52
|
-
- `APX.toast.
|
|
53
|
-
- `APX.toast.
|
|
54
|
-
- `APX.toast.
|
|
55
|
-
- `APX.toast.
|
|
56
|
-
- `APX.toast.
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
-
|
|
134
|
-
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
-
|
|
142
|
-
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
1
|
+
# APX Toast
|
|
2
|
+
|
|
3
|
+
A tiny, framework‑agnostic toast library for APX. Minimal CSS, ESM‑first, no globals. DOM is only touched when you actually show a toast (SSR‑safe to import).
|
|
4
|
+
|
|
5
|
+
## Install / Import
|
|
6
|
+
|
|
7
|
+
Just import `APX` — the toast CSS is automatically loaded by `modules/toast/toast.mjs`.
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<script type="module">
|
|
11
|
+
import APX from './APX.mjs';
|
|
12
|
+
// CSS is auto‑imported; no <link> tag required.
|
|
13
|
+
// ...
|
|
14
|
+
APX.toast.success('Ready!');
|
|
15
|
+
</script>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
// Default manager (lazy)
|
|
22
|
+
APX.toast.success('Saved!');
|
|
23
|
+
APX.toast.warning('Heads up', { durationMs: 4000 });
|
|
24
|
+
APX.toast.danger('Something failed', { durationMs: 0 }); // sticky
|
|
25
|
+
|
|
26
|
+
// Custom toast
|
|
27
|
+
const ref = APX.toast.show({ message: 'Processing…', type: 'info', durationMs: 0 });
|
|
28
|
+
// Callable shorthand for show:
|
|
29
|
+
APX.toast({ message: 'Hello', type: 'success' });
|
|
30
|
+
ref.update({ message: 'Done', type: 'success', durationMs: 1800 });
|
|
31
|
+
ref.whenClosed().then(() => console.log('closed'));
|
|
32
|
+
|
|
33
|
+
// Configure defaults at runtime
|
|
34
|
+
APX.toast.configure({ position: 'top-right', maxToasts: 4, dedupe: true });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Named managers (profiles)
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// Register a named manager
|
|
41
|
+
APX.toast.create('admin', { position: 'bottom-left', ariaLive: 'polite' });
|
|
42
|
+
|
|
43
|
+
// Use it later
|
|
44
|
+
APX.toast.use('admin').info('Admin ready');
|
|
45
|
+
APX.toast.use('admin').closeAll();
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## API overview
|
|
49
|
+
|
|
50
|
+
- Top‑level (default manager):
|
|
51
|
+
- `APX.toast(opts)` — callable shorthand for `show(opts)`
|
|
52
|
+
- `APX.toast.show(opts)`
|
|
53
|
+
- `APX.toast.info|success|warning|danger(message, opts?)`
|
|
54
|
+
- `APX.toast.configure(config)`
|
|
55
|
+
- `APX.toast.closeAll(reason?)` — `reason`: `'api'` | `'overflow'`
|
|
56
|
+
- `APX.toast.getOpenToasts()` — returns `ToastRef[]`
|
|
57
|
+
- `APX.toast.create(name, config)` — register named manager; or `create(config)` to get a new manager without registering
|
|
58
|
+
- `APX.toast.use(name)` — returns named manager or `null`
|
|
59
|
+
|
|
60
|
+
- Named manager instance (same surface):
|
|
61
|
+
- `.show(opts)`, `.info|success|warning|danger(message, opts?)`
|
|
62
|
+
- `.configure(config)`, `.closeAll(reason?)`, `.getOpenToasts()`
|
|
63
|
+
|
|
64
|
+
- **ToastRef** (returned by `show` / helpers):
|
|
65
|
+
- `ref.id`, `ref.el` (HTMLElement)
|
|
66
|
+
- `ref.close(reason?)` — `reason`: `'api'` | `'close'`
|
|
67
|
+
- `ref.update(partial)` — merge options (message, type, durationMs, progress, className, etc.)
|
|
68
|
+
- `ref.whenClosed()` — `Promise<void>` when toast is removed
|
|
69
|
+
- `ref.on('close'|'click', handler)` — returns unsubscribe function
|
|
70
|
+
- `ref.off('close'|'click', handler)`
|
|
71
|
+
|
|
72
|
+
## Options and types
|
|
73
|
+
|
|
74
|
+
```js
|
|
75
|
+
// ToastConfig (global/manager defaults)
|
|
76
|
+
{
|
|
77
|
+
position: Position, // default 'bottom-right' (string or object, see Position below)
|
|
78
|
+
flow: 'up'|'down'|'auto', // stacking direction; 'auto' from position (default 'auto')
|
|
79
|
+
maxToasts: number, // default 5
|
|
80
|
+
defaultDurationMs: number, // default 5000
|
|
81
|
+
zIndex: number, // default 11000
|
|
82
|
+
ariaLive: 'polite'|'assertive'|'off', // default 'polite'
|
|
83
|
+
gap: number, // default 8
|
|
84
|
+
dedupe: boolean, // default false
|
|
85
|
+
containerClass: string, // extra class on container
|
|
86
|
+
offset: number, // px offset from screen edges (string positions only)
|
|
87
|
+
id: string, // default id for toasts when not provided per-call
|
|
88
|
+
progress: false | true | { enable, position?, pauseButton? } // default false; pauseButton default false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Position — string or object
|
|
92
|
+
// String: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left'
|
|
93
|
+
// Object:
|
|
94
|
+
// - type 'sticky': fixed viewport, use x / y (e.g. '10px', '-20px' = from right/bottom)
|
|
95
|
+
// - type 'relative': relative to element; x, y offset; requires element
|
|
96
|
+
// - type 'anchored': outside element; placement 'top'|'right'|'bottom'|'left' (or 'above'|'below'|'before'|'after'); gap; requires element
|
|
97
|
+
// - useNativeCSS: true — for relative/anchored, render container inside the element with CSS (no fixed overlay)
|
|
98
|
+
|
|
99
|
+
// ToastOptions (per toast)
|
|
100
|
+
{
|
|
101
|
+
message: string | Node,
|
|
102
|
+
type: 'info'|'success'|'warning'|'danger', // default 'info'
|
|
103
|
+
durationMs: number, // default from config; 0 = sticky
|
|
104
|
+
dismissible: boolean, // default true
|
|
105
|
+
id: string, // stable id for dedupe updates
|
|
106
|
+
className: string, // extra classes on the toast element
|
|
107
|
+
position: Position, // override container position for this toast
|
|
108
|
+
flow: 'up'|'down'|'auto', // override stacking direction for this toast
|
|
109
|
+
progress: true | { enable: boolean, position?: 'top'|'bottom', pauseButton?: boolean }, // pauseButton default false
|
|
110
|
+
onClick: (ref, ev) => void,
|
|
111
|
+
onClose: (ref, reason) => void // reason: 'timeout'|'close'|'api'|'overflow'
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## Position (advanced)
|
|
116
|
+
|
|
117
|
+
Besides the four corners (`'bottom-right'`, `'bottom-left'`, `'top-right'`, `'top-left'`), position can be an object:
|
|
118
|
+
|
|
119
|
+
- **sticky** (or omit type and set `x`/`y`): fixed in viewport. Use `x`, `y` as CSS values; prefix with `-` for right/bottom (e.g. `y: '-20px'`).
|
|
120
|
+
- **relative**: fixed overlay positioned relative to an element. Requires `element` (HTMLElement); `x`, `y` are offsets (px/em). Container follows scroll/resize.
|
|
121
|
+
- **anchored**: container placed outside the element. Requires `element` and `placement` (`'top'|'right'|'bottom'|'left'` or synonyms `'above'|'below'|'before'|'after'`). Optional `gap` (e.g. `'1em'`).
|
|
122
|
+
- **useNativeCSS**: when `true` with relative/anchored, the toast container is rendered inside the target element with `position: absolute` (no fixed overlay). The target gets `position: relative` while toasts exist.
|
|
123
|
+
|
|
124
|
+
`flow` (`'up'|'down'|'auto'`) controls stacking: `'auto'` is derived from position (e.g. top → up, bottom → down).
|
|
125
|
+
|
|
126
|
+
## Theming (minimal CSS)
|
|
127
|
+
|
|
128
|
+
Override CSS variables to theme without touching markup:
|
|
129
|
+
|
|
130
|
+
```css
|
|
131
|
+
:root {
|
|
132
|
+
--apx-toast-gap: 10px;
|
|
133
|
+
--apx-toast-min-width: 280px;
|
|
134
|
+
--apx-toast-radius: 8px;
|
|
135
|
+
--apx-toast-success-bg: #16a34a;
|
|
136
|
+
--apx-toast-success-fg: #fff;
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Class structure (BEM‑like):
|
|
141
|
+
- Container: `APX-toast-container APX-toast-container--{corner}`
|
|
142
|
+
- Toast: `APX-toast APX-toast--{type}`
|
|
143
|
+
- Children: `APX-toast__content`, optional `APX-toast__close`
|
|
144
|
+
- Progress (v2.6.1): `APX-toast__progress`, `APX-toast__progress-track`, `APX-toast__progress-bar`, optional `APX-toast__progress-pause`
|
|
145
|
+
- Animations: `APX-toast--enter/--enter-active`, `APX-toast--exit/--exit-active`
|
|
146
|
+
|
|
147
|
+
## Progress bar (v2.6.1)
|
|
148
|
+
|
|
149
|
+
Optional visual countdown for toasts with a duration. Bar shows time remaining (100% → 0%); sync with timer and hover pause.
|
|
150
|
+
|
|
151
|
+
```js
|
|
152
|
+
// Bar on top (default), no pause button by default
|
|
153
|
+
APX.toast.show({ message: 'Saving…', type: 'info', progress: true });
|
|
154
|
+
|
|
155
|
+
// Bar with pause/resume button
|
|
156
|
+
APX.toast.show({ message: 'Pausable', type: 'info', progress: { enable: true, position: 'top', pauseButton: true } });
|
|
157
|
+
|
|
158
|
+
// Bar at bottom
|
|
159
|
+
APX.toast.show({ message: 'Done', type: 'success', progress: { enable: true, position: 'bottom' } });
|
|
160
|
+
|
|
161
|
+
// Default progress for all toasts from a manager
|
|
162
|
+
APX.toast.configure({ progress: true });
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- `progress: true` → bar at top, no pause button.
|
|
166
|
+
- `progress: { enable, position: 'top'|'bottom', pauseButton?: boolean }` — `pauseButton` defaults to `false`; set `pauseButton: true` to show the round pause/resume button on the toast corner.
|
|
167
|
+
- No bar if `durationMs === 0` (sticky). Extra spacing is applied when the button is present so stacked toasts do not overlap.
|
|
168
|
+
|
|
169
|
+
## Behavior
|
|
170
|
+
|
|
171
|
+
- Lazy container creation (first `show`).
|
|
172
|
+
- `maxToasts` enforced; oldest removed with reason `'overflow'`.
|
|
173
|
+
- Hover pauses timer; resumes on mouse leave (unless user clicked pause).
|
|
174
|
+
- `durationMs = 0` makes the toast sticky (no progress bar).
|
|
175
|
+
- If `dedupe: true` and `id` matches an open toast, it updates instead of creating a new one.
|
|
176
|
+
- With `progress`, a bar and optional pause/resume button show; button toggles pause (same as hover).
|
|
177
|
+
|
|
178
|
+
## Accessibility & SSR
|
|
179
|
+
|
|
180
|
+
- Container uses `aria-live` (configurable).
|
|
181
|
+
- Each toast has `role="status"`.
|
|
182
|
+
- ESM only; no DOM access at import time. Safe to import in SSR; DOM is touched only when showing toasts in the browser.
|
|
183
|
+
|
|
184
|
+
## License
|
|
185
|
+
|
|
186
|
+
Copyright Appius.
|
|
@@ -123,12 +123,14 @@ The module intelligently handles mixed structures (objects with both numeric and
|
|
|
123
123
|
|
|
124
124
|
## API
|
|
125
125
|
|
|
126
|
-
### `APX.tools.packFormToJSON(form)`
|
|
126
|
+
### `APX.tools.packFormToJSON(form, options?)`
|
|
127
127
|
|
|
128
128
|
Converts an HTML form element into a JSON object.
|
|
129
129
|
|
|
130
130
|
**Parameters:**
|
|
131
131
|
- `form` (HTMLFormElement): The form element to convert
|
|
132
|
+
- `options` (Object, optional):
|
|
133
|
+
- `numericKeysAlwaysArray` (boolean, default `false`): If `true`, every numeric bracket key (e.g. `[26]`) is treated as an array index (pre-2.6.2 behaviour). If `false`, the heuristic applies: array only when indices are dense 0,1,2,…,n; otherwise object with string keys.
|
|
132
134
|
|
|
133
135
|
**Returns:** (Object) The JSON object representation of the form data
|
|
134
136
|
|
|
@@ -137,30 +139,26 @@ Converts an HTML form element into a JSON object.
|
|
|
137
139
|
**Example:**
|
|
138
140
|
```javascript
|
|
139
141
|
const form = document.querySelector('#myForm');
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
} catch (error) {
|
|
144
|
-
console.error('Error:', error.message);
|
|
145
|
-
}
|
|
142
|
+
const data = APX.tools.packFormToJSON(form);
|
|
143
|
+
// Legacy: always use arrays for numeric keys
|
|
144
|
+
const dataLegacy = APX.tools.packFormToJSON(form, { numericKeysAlwaysArray: true });
|
|
146
145
|
```
|
|
147
146
|
|
|
148
|
-
### `APX('form').pack()`
|
|
147
|
+
### `APX('form').pack(options?)`
|
|
149
148
|
|
|
150
149
|
Converts the first selected form element into a JSON object. This is a chainable method on APX objects.
|
|
151
150
|
|
|
151
|
+
**Parameters:**
|
|
152
|
+
- `options` (Object, optional): Same as the second argument of `packFormToJSON` (e.g. `{ numericKeysAlwaysArray: true }`).
|
|
153
|
+
|
|
152
154
|
**Returns:** (Object) The JSON object representation of the form data
|
|
153
155
|
|
|
154
156
|
**Throws:** (Error) If no element is found or the first element is not a form
|
|
155
157
|
|
|
156
158
|
**Example:**
|
|
157
159
|
```javascript
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
console.log(data);
|
|
161
|
-
} catch (error) {
|
|
162
|
-
console.error('Error:', error.message);
|
|
163
|
-
}
|
|
160
|
+
const data = APX('form#myForm').pack();
|
|
161
|
+
const dataLegacy = APX('form#myForm').pack({ numericKeysAlwaysArray: true });
|
|
164
162
|
```
|
|
165
163
|
|
|
166
164
|
---
|
|
@@ -8,12 +8,14 @@ import { packFormToJSON } from './packToJson.mjs';
|
|
|
8
8
|
export default function (apx) {
|
|
9
9
|
/**
|
|
10
10
|
* Convertit le premier formulaire sélectionné en objet JSON
|
|
11
|
+
* @param {Object} [options] - Options passées à packFormToJSON (ex. { numericKeysAlwaysArray: true })
|
|
11
12
|
* @returns {Object} L'objet JSON résultant
|
|
12
13
|
* @throws {Error} Si aucun formulaire n'est trouvé ou si le premier élément n'est pas un formulaire
|
|
13
14
|
* @example
|
|
14
15
|
* const data = APX('form.myformclass').pack();
|
|
16
|
+
* const dataLegacy = APX('form').pack({ numericKeysAlwaysArray: true });
|
|
15
17
|
*/
|
|
16
|
-
apx.pack = function () {
|
|
18
|
+
apx.pack = function (options) {
|
|
17
19
|
const firstElement = this.first();
|
|
18
20
|
if (!firstElement) {
|
|
19
21
|
throw new Error('No element found');
|
|
@@ -21,7 +23,7 @@ export default function (apx) {
|
|
|
21
23
|
if (firstElement.tagName !== 'FORM') {
|
|
22
24
|
throw new Error('Element is not a form');
|
|
23
25
|
}
|
|
24
|
-
return packFormToJSON(firstElement);
|
|
26
|
+
return packFormToJSON(firstElement, options);
|
|
25
27
|
};
|
|
26
28
|
|
|
27
29
|
return apx;
|