@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
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Convertit un formulaire HTML en objet JSON
|
|
3
3
|
* @param {HTMLFormElement} form - Le formulaire à convertir
|
|
4
|
+
* @param {Object} [options] - Options de conversion
|
|
5
|
+
* @param {boolean} [options.numericKeysAlwaysArray=false] - Si true, toute clé numérique est traitée comme index de tableau (comportement d'avant 2.6.2). Sinon, heuristique : tableau seulement si indices denses 0,1,2,…,n.
|
|
4
6
|
* @returns {Object} L'objet JSON résultant
|
|
5
7
|
* @throws {TypeError} Si form n'est pas un HTMLFormElement
|
|
6
8
|
*/
|
|
7
|
-
export const packFormToJSON = (form) => {
|
|
9
|
+
export const packFormToJSON = (form, options = {}) => {
|
|
8
10
|
// Validation de l'entrée
|
|
9
11
|
if (!form || !(form instanceof HTMLFormElement)) {
|
|
10
12
|
throw new TypeError('packFormToJSON expects an HTMLFormElement');
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
const numericKeysAlwaysArray = options.numericKeysAlwaysArray === true;
|
|
16
|
+
|
|
13
17
|
const formData = new FormData(form);
|
|
14
18
|
const jsonData = {};
|
|
15
19
|
|
|
@@ -48,6 +52,7 @@ export const packFormToJSON = (form) => {
|
|
|
48
52
|
|
|
49
53
|
const pathUsage = new Map(); // Map<pathString, PathUsage>
|
|
50
54
|
const keyAnalysis = new Map(); // Map<basePath, {hasNumeric: boolean, hasString: boolean}>
|
|
55
|
+
const numericIndicesByPath = new Map(); // Map<pathString, Set<number>> — indices under each parent path
|
|
51
56
|
const allEntries = [];
|
|
52
57
|
|
|
53
58
|
/**
|
|
@@ -100,6 +105,13 @@ export const packFormToJSON = (form) => {
|
|
|
100
105
|
if (part.type === 'key') {
|
|
101
106
|
currentPath = currentPath ? `${currentPath}[${part.name}]` : part.name;
|
|
102
107
|
} else if (part.type === 'numeric') {
|
|
108
|
+
const parentPath = currentPath;
|
|
109
|
+
let set = numericIndicesByPath.get(parentPath);
|
|
110
|
+
if (!set) {
|
|
111
|
+
set = new Set();
|
|
112
|
+
numericIndicesByPath.set(parentPath, set);
|
|
113
|
+
}
|
|
114
|
+
set.add(part.index);
|
|
103
115
|
currentPath = `${currentPath}[${part.index}]`;
|
|
104
116
|
} else if (part.type === 'array') {
|
|
105
117
|
currentPath = `${currentPath}[]`;
|
|
@@ -190,6 +202,34 @@ export const packFormToJSON = (form) => {
|
|
|
190
202
|
return analysis?.hasNumeric && analysis?.hasString;
|
|
191
203
|
};
|
|
192
204
|
|
|
205
|
+
/**
|
|
206
|
+
* True if indices are exactly 0, 1, 2, ..., n (dense, sequential from 0).
|
|
207
|
+
* @param {number[]} indices - Sorted array of indices
|
|
208
|
+
* @returns {boolean}
|
|
209
|
+
*/
|
|
210
|
+
const isDenseSequential = (indices) => {
|
|
211
|
+
if (indices.length === 0) return false;
|
|
212
|
+
const sorted = [...indices].sort((a, b) => a - b);
|
|
213
|
+
for (let j = 0; j < sorted.length; j++) {
|
|
214
|
+
if (sorted[j] !== j) return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* True if numeric keys under this path should be treated as array indices.
|
|
221
|
+
* With numericKeysAlwaysArray: always true when path has numeric children.
|
|
222
|
+
* Otherwise: true only when indices are dense 0..n.
|
|
223
|
+
* @param {string} parentPath - Path to the container that has numeric children
|
|
224
|
+
* @returns {boolean}
|
|
225
|
+
*/
|
|
226
|
+
const useArrayForNumericPath = (parentPath) => {
|
|
227
|
+
const set = numericIndicesByPath.get(parentPath);
|
|
228
|
+
if (!set || set.size === 0) return true;
|
|
229
|
+
if (numericKeysAlwaysArray) return true;
|
|
230
|
+
return isDenseSequential([...set]);
|
|
231
|
+
};
|
|
232
|
+
|
|
193
233
|
/**
|
|
194
234
|
* Convertit un tableau en objet en préservant les indices numériques comme propriétés
|
|
195
235
|
* @param {Array} arr - Le tableau à convertir
|
|
@@ -285,8 +325,9 @@ export const packFormToJSON = (form) => {
|
|
|
285
325
|
* @param {*} value - La valeur à assigner
|
|
286
326
|
* @param {Object} parent - Le conteneur parent
|
|
287
327
|
* @param {string} basePath - Le chemin de base pour la détection de conflit
|
|
328
|
+
* @param {string} parentPath - Chemin du conteneur actuel (pour heuristique numeric)
|
|
288
329
|
*/
|
|
289
|
-
const processFinalValue = (container, part, value, parent, basePath) => {
|
|
330
|
+
const processFinalValue = (container, part, value, parent, basePath, parentPath) => {
|
|
290
331
|
if (part.type === 'array') {
|
|
291
332
|
// Tableau explicite avec []
|
|
292
333
|
container[part.name] ??= [];
|
|
@@ -296,13 +337,18 @@ export const packFormToJSON = (form) => {
|
|
|
296
337
|
container[part.name] = [container[part.name], value];
|
|
297
338
|
}
|
|
298
339
|
} else if (part.type === 'numeric') {
|
|
299
|
-
// Indice numérique final
|
|
300
340
|
const { index } = part;
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
container.
|
|
341
|
+
const useArray = useArrayForNumericPath(parentPath);
|
|
342
|
+
if (useArray) {
|
|
343
|
+
container = ensureArray(container, parent.key, parent.container);
|
|
344
|
+
while (container.length <= index) {
|
|
345
|
+
container.push(undefined);
|
|
346
|
+
}
|
|
347
|
+
container[index] = value;
|
|
348
|
+
} else {
|
|
349
|
+
container = ensureObject(container, parent.key, parent.container, true);
|
|
350
|
+
container[index] = value;
|
|
304
351
|
}
|
|
305
|
-
container[index] = value;
|
|
306
352
|
} else {
|
|
307
353
|
// Clé simple finale
|
|
308
354
|
const conflict = hasConflict(basePath);
|
|
@@ -348,16 +394,14 @@ export const packFormToJSON = (form) => {
|
|
|
348
394
|
*/
|
|
349
395
|
const processIntermediatePart = (container, part, nextPart, parent, basePath, parts, i, key) => {
|
|
350
396
|
if (part.type === 'numeric') {
|
|
351
|
-
// Indice numérique : le container doit être un tableau ou un objet (selon conflit)
|
|
352
397
|
const { index } = part;
|
|
353
|
-
const
|
|
398
|
+
const parentPath = buildPathString(parts.slice(0, i));
|
|
399
|
+
const useArray = useArrayForNumericPath(parentPath) && !hasConflict(basePath);
|
|
354
400
|
|
|
355
|
-
if (
|
|
356
|
-
// Conflit : utiliser un objet (les indices seront des propriétés)
|
|
401
|
+
if (!useArray) {
|
|
357
402
|
container = ensureObject(container, parent.key, parent.container, true);
|
|
358
403
|
container[index] ??= {};
|
|
359
404
|
if (typeof container[index] !== 'object' || container[index] === null) {
|
|
360
|
-
// Cette erreur ne devrait jamais se produire si la détection fonctionne correctement
|
|
361
405
|
const pathParts = parts.slice(0, i + 1);
|
|
362
406
|
const currentPath = buildPathString(pathParts);
|
|
363
407
|
throw new Error(
|
|
@@ -367,14 +411,12 @@ export const packFormToJSON = (form) => {
|
|
|
367
411
|
);
|
|
368
412
|
}
|
|
369
413
|
} else {
|
|
370
|
-
// Pas de conflit : utiliser un tableau
|
|
371
414
|
container = ensureArray(container, parent.key, parent.container);
|
|
372
415
|
while (container.length <= index) {
|
|
373
416
|
container.push(undefined);
|
|
374
417
|
}
|
|
375
418
|
container[index] ??= {};
|
|
376
419
|
if (typeof container[index] !== 'object' || container[index] === null) {
|
|
377
|
-
// Cette erreur ne devrait jamais se produire si la détection fonctionne correctement
|
|
378
420
|
const pathParts = parts.slice(0, i + 1);
|
|
379
421
|
const currentPath = buildPathString(pathParts);
|
|
380
422
|
throw new Error(
|
|
@@ -459,8 +501,8 @@ export const packFormToJSON = (form) => {
|
|
|
459
501
|
const parent = path[path.length - 1];
|
|
460
502
|
|
|
461
503
|
if (isLast) {
|
|
462
|
-
|
|
463
|
-
processFinalValue(container, part, value, parent, basePath);
|
|
504
|
+
const parentPath = buildPathString(parts.slice(0, i));
|
|
505
|
+
processFinalValue(container, part, value, parent, basePath, parentPath);
|
|
464
506
|
} else {
|
|
465
507
|
// Partie intermédiaire : créer la structure
|
|
466
508
|
const newContainer = processIntermediatePart(container, part, nextPart, parent, basePath, parts, i, key);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Tristate Module Changelog
|
|
2
|
+
|
|
3
|
+
This changelog tracks updates for `modules/tristate` between APX releases.
|
|
4
|
+
|
|
5
|
+
## TBA (next release)
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **Chainable API**: `tristate(options)` now returns an object with `setChildren(childrenApx)`. Use it to link a parent tristate to its child checkboxes.
|
|
9
|
+
- **Parent + sub-checkboxes**: `setChildren(childrenApx)` wires one parent (first in the selection) to all child checkboxes. Parent reflects children (all checked → parent checked, all crossed → parent crossed, all unchecked → parent empty, **mixed → parent shows indeterminate dash**). Clicking the parent applies its new state to all children. Children are initialized as tristates with the same options as the parent.
|
|
10
|
+
- **Automatic indeterminate (dash)**: When `setChildren()` is used, the parent automatically gets the indeterminate (dash) appearance **only when children are mixed**. When all children are unchecked the parent shows an empty frame. Indeterminate is no longer a manual option; it is derived from child states.
|
|
11
|
+
- New `colors` option: an object grouping color targets.
|
|
12
|
+
- `colors.tick`: checked/crossed symbol color (default: `transparent`).
|
|
13
|
+
- `colors.checked`: checked state background (default: `#0654ba`).
|
|
14
|
+
- `colors.crossed`: crossed state background (default: `#f00`).
|
|
15
|
+
- Flat options `tickColor`, `checkedColor`, `crossedColor` remain supported for backward compatibility; `colors` takes precedence when both are set.
|
|
16
|
+
- CSS class `apx-tristate--mixed`: when the parent has indeterminate-dash and `--mixed`, the dash is shown; when not `--mixed` (e.g. all children unchecked), the parent shows an empty unchecked frame. Used internally by `setChildren()`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
- `size.width` and `size.height` accept numbers (treated as pixels) or strings (used as CSS length, e.g. `'1.25rem'`, `'20px'`). Invalid types throw `TypeError`.
|
|
20
|
+
- Tristate symbols (check and cross) are rendered with `mask-image` and data URIs over themeable backgrounds. Unchecked and indeterminate-dash assets are inlined as data URIs in CSS so the demo works from `file://` without CORS.
|
|
21
|
+
- Unchecked icon uses a rounded inset (`4px`). Indeterminate (dash) uses checked background + tick-colored dash; indeterminate (filled check) uses checked background only with `border-radius: inherit`.
|
|
22
|
+
- **State classes**: Only one of `unchecked` / `checked` / `crossed` is ever set on the tristate DOM (fixes parent with `setChildren` incorrectly showing indeterminate when state was `crossed`). `setDefaultState` and `toggleTriStateCheckboxState` now clear all three before adding the active one.
|
|
23
|
+
- Asset `tristate-indeterminate.svg` renamed to `tristate-indeterminate-dash.svg`.
|
|
24
|
+
|
|
25
|
+
### Removed
|
|
26
|
+
- **Breaking**: Options `uncheckedAppearance` and `indeterminateDisplay` have been removed. Indeterminate (dash) is no longer configurable manually; it is applied automatically on parents that use `setChildren()` when their children are in a mixed state.
|
|
27
|
+
|
|
28
|
+
### Docs & Demo
|
|
29
|
+
- Demo `demo/modules/tristate/index.html`: Parameters (size + colors), Live preview, States preview, Interactive cycle, **Parent + sub-checkboxes** real-world section using `APX('#policyParent').tristate(opts).setChildren(APX('#policyEmail, #policySms, #policyPush'))`. Removed manual uncheckedAppearance/indeterminateDisplay controls and the separate “Unchecked appearance” preview section.
|
|
30
|
+
- README: documented `setChildren(childrenApx)`, automatic indeterminate when mixed, and removed `uncheckedAppearance` / `indeterminateDisplay` from the options list.
|
|
31
|
+
|
|
32
|
+
## 2.6.2 (current)
|
|
33
|
+
|
|
34
|
+
Baseline for this changelog. Tristate visuals and options prior to the theming improvements above.
|
|
@@ -1,95 +1,158 @@
|
|
|
1
|
-
# APX Tri-State Checkbox Module
|
|
2
|
-
|
|
3
|
-
The APX Tri-State Checkbox module provides an augmentation to the `APX` object. This functionality allows you to easily transform standard checkboxes within an `APX` object into tri-state checkboxes.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
1
|
+
# APX Tri-State Checkbox Module
|
|
2
|
+
|
|
3
|
+
The APX Tri-State Checkbox module provides an augmentation to the `APX` object. This functionality allows you to easily transform standard checkboxes within an `APX` object into tri-state checkboxes.
|
|
4
|
+
|
|
5
|
+
### When to use this module
|
|
6
|
+
|
|
7
|
+
This module is designed for **whitelist/blacklist** and **inheritance-based policy** UIs: permissions, feature flags, or rule matrices where each row can be explicitly allowed, explicitly denied, or left unspecified (inherit/default). Three distinct states avoid forcing a binary choice and keep policy intent clear both in the UI and when submitting to the backend.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
Ensure you have the core `APX` object integrated into your project.
|
|
12
|
+
|
|
13
|
+
Next, to use this augmentation, make sure you've imported and added the `tristate` module to your project:
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import APX from 'path-to-APX';
|
|
17
|
+
// Assuming tristate is part of APX's module structure
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Also, link the required CSS:
|
|
21
|
+
|
|
22
|
+
```html
|
|
23
|
+
<link rel="stylesheet" href="path-to-css/tristate.css">
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Usage
|
|
27
|
+
|
|
28
|
+
To transform a group of checkboxes into tri-state checkboxes using the `APX` object, first create an `APX` object containing the checkboxes you wish to transform, then simply call the `tristate` method:
|
|
29
|
+
|
|
30
|
+
```javascript
|
|
31
|
+
const myApxCheckboxes = APX('.my-checkbox-class');
|
|
32
|
+
myApxCheckboxes.tristate();
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Example: policy row (whitelist/blacklist)** — one checkbox per permission, submitted as allow/deny/inherit:
|
|
36
|
+
|
|
37
|
+
```html
|
|
38
|
+
<form>
|
|
39
|
+
<label>Can edit <input type="checkbox" name="perm_edit" class="policy-cb"></label>
|
|
40
|
+
<label>Can delete <input type="checkbox" name="perm_delete" class="policy-cb"></label>
|
|
41
|
+
</form>
|
|
42
|
+
<script>
|
|
43
|
+
APX('.policy-cb').tristate({
|
|
44
|
+
defaultState: 'unchecked',
|
|
45
|
+
colors: { tick: '#fff' },
|
|
46
|
+
callbacks: { change: (_, el, hidden) => console.log(hidden.name, hidden.value || 'inherit'); }
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Parameters
|
|
52
|
+
|
|
53
|
+
The `tristate` method accepts an `options` parameter which can have the following properties:
|
|
54
|
+
|
|
55
|
+
- `size`: An object that defines the `width` and `height` of the tri-state checkbox.
|
|
56
|
+
- `width`: Numeric value is treated as pixels (e.g. `50` → `50px`); string values are used as-is (e.g. `'1.25rem'`, `'20px'`).
|
|
57
|
+
- `height`: Same as `width` (number = px, string = any CSS length).
|
|
58
|
+
|
|
59
|
+
- `classes`: A string or an array of strings representing classes to be added to the custom checkbox. The string can contain class names separated by spaces or commas.
|
|
60
|
+
|
|
61
|
+
- `defaultState`: A string that defines the default state of the tri-state checkbox. Possible values are 'checked', 'unchecked', and 'crossed'.
|
|
62
|
+
|
|
63
|
+
- `colors`: An object grouping color targets (recommended). Omitted keys keep CSS defaults.
|
|
64
|
+
- `tick`: CSS color for the check/cross symbol. Default is `transparent`.
|
|
65
|
+
- `checked`: CSS color for the checked state background. Default is `#0654ba`.
|
|
66
|
+
- `crossed`: CSS color for the crossed state background. Default is `#f00`.
|
|
67
|
+
- For backward compatibility, the flat options `tickColor`, `checkedColor`, and `crossedColor` are still supported; they are overridden by `colors` when both are present.
|
|
68
|
+
|
|
69
|
+
- `callbacks`: An object that contains callback functions.
|
|
70
|
+
- `after`: A function that is called after the tri-state checkbox is created.
|
|
71
|
+
- `change`: A function that is called when the tri-state checkbox state changes.
|
|
72
|
+
|
|
73
|
+
- `bubbleEvents`: A boolean that determines whether events (`click`, `keyup`, `change`) on the custom checkbox should bubble up to the original checkbox.
|
|
74
|
+
|
|
75
|
+
### Parent + sub-checkboxes: `setChildren(childrenApx)`
|
|
76
|
+
|
|
77
|
+
`tristate()` returns a chainable object with a `setChildren` method. Use it to link one parent tristate to its child checkboxes. The parent then reflects the children (all checked → parent checked, all crossed → parent crossed, all unchecked → parent empty, **mixed children → parent shows indeterminate dash**). Clicking the parent applies its new state to all children.
|
|
78
|
+
|
|
79
|
+
The indeterminate (dash) appearance is **automatically** applied to the parent when `setChildren` is used: the dash is shown only when the children are in a mixed state; when all are unchecked the parent shows an empty frame.
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
APX('#policyParent').tristate({ size: { width: 28, height: 28 }, colors: { tick: '#fff' }, defaultState: 'unchecked' })
|
|
83
|
+
.setChildren(APX('#policyEmail, #policySms, #policyPush'));
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
- Only the **first** parent in the APX selection is linked to **all** children in the given APX. For multiple parent/child groups, call `tristate().setChildren()` per parent.
|
|
87
|
+
- Children are initialized as tristates with the same options as the parent (plus internal change callbacks). If they are already tristates, they are just linked.
|
|
88
|
+
|
|
89
|
+
Example:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
const options = {
|
|
93
|
+
size: {
|
|
94
|
+
width: 50,
|
|
95
|
+
height: 30
|
|
96
|
+
},
|
|
97
|
+
classes: 'myClass1 myClass2',
|
|
98
|
+
defaultState: 'checked',
|
|
99
|
+
callbacks: {
|
|
100
|
+
after: function(originalCheckbox, tristateDom, hiddenInput) {
|
|
101
|
+
console.log("Checkbox created!");
|
|
102
|
+
},
|
|
103
|
+
change: function(originalCheckbox, tristateDom, hiddenInput) {
|
|
104
|
+
console.log("Checkbox state changed!");
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
colors: {
|
|
108
|
+
tick: '#ffffff',
|
|
109
|
+
checked: '#0654ba',
|
|
110
|
+
crossed: '#ff0000'
|
|
111
|
+
},
|
|
112
|
+
bubbleEvents: true
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const myApxCheckboxes = APX('.my-checkbox-class');
|
|
116
|
+
myApxCheckboxes.tristate(options);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Features
|
|
120
|
+
|
|
121
|
+
### Tri-State Logic
|
|
122
|
+
|
|
123
|
+
The checkbox will cycle through three states:
|
|
124
|
+
|
|
125
|
+
1. Checked (represented by a tick)
|
|
126
|
+
2. Crossed (represented by a cross)
|
|
127
|
+
3. Unchecked (default checkbox state)
|
|
128
|
+
|
|
129
|
+
### Whitelist/Blacklist semantics
|
|
130
|
+
|
|
131
|
+
For policy UIs, map the three states as follows:
|
|
132
|
+
|
|
133
|
+
| State | Visual | Typical meaning | Backend value (via hidden input) |
|
|
134
|
+
|-----------|--------|-------------------------|----------------------------------|
|
|
135
|
+
| Checked | Tick | Allow / whitelist | `true` (or your chosen value) |
|
|
136
|
+
| Crossed | Cross | Deny / blacklist | `false` (or your chosen value) |
|
|
137
|
+
| Unchecked | Empty | Inherit / default / N/A | Not submitted (no value) |
|
|
138
|
+
|
|
139
|
+
This gives you an explicit **deny vs not-set** distinction: users can leave a row unspecified instead of being forced to choose allow or deny.
|
|
140
|
+
|
|
141
|
+
### Benefits
|
|
142
|
+
|
|
143
|
+
- **Explicit allow/deny vs inherit** — Unchecked means “no override”; checked and crossed mean explicit policy decisions.
|
|
144
|
+
- **Cleaner inheritance** — Hierarchical rules (e.g. org → team → user) can override only where needed; other levels inherit.
|
|
145
|
+
- **Auditability** — It is clear in the UI and in submitted data whether a rule was explicitly denied or simply not set.
|
|
146
|
+
|
|
147
|
+
### Accessibility
|
|
148
|
+
|
|
149
|
+
The tri-state checkbox retains the `tabIndex` property of the original checkbox, ensuring the accessibility of the control.
|
|
150
|
+
|
|
151
|
+
### Event Bubbling
|
|
152
|
+
|
|
153
|
+
Any `click`, `keyup`, or `change` events attached to the original checkbox will be bubbled up from the custom checkbox if the `bubbleEvents` option is set to true. This ensures that your existing event listeners will still work as expected and that custom callbacks can also be executed.
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
95
158
|
Copyright Appius SARL
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
2
|
+
<mask id="frame-cutout">
|
|
3
|
+
<rect width="32" height="32" fill="#fff" />
|
|
4
|
+
<rect x="3" y="3" width="26" height="26" rx="4" ry="4" fill="#000" />
|
|
5
|
+
</mask>
|
|
6
|
+
<rect width="32" height="32" fill="#cccccc" mask="url(#frame-cutout)" />
|
|
7
|
+
</svg>
|
|
@@ -1,25 +1,92 @@
|
|
|
1
|
-
.apx-tristate {
|
|
2
|
-
display: inline-block;
|
|
3
|
-
vertical-align: middle;
|
|
4
|
-
text-align: center;
|
|
5
|
-
cursor: pointer;
|
|
6
|
-
position: relative;
|
|
7
|
-
min-width:13px;
|
|
8
|
-
min-height:13px;
|
|
9
|
-
background-size: contain;
|
|
10
|
-
background-color:
|
|
11
|
-
border-radius:3px;
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
.apx-tristate.
|
|
17
|
-
background-image: url(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
.apx-tristate {
|
|
2
|
+
display: inline-block;
|
|
3
|
+
vertical-align: middle;
|
|
4
|
+
text-align: center;
|
|
5
|
+
cursor: pointer;
|
|
6
|
+
position: relative;
|
|
7
|
+
min-width:13px;
|
|
8
|
+
min-height:13px;
|
|
9
|
+
background-size: contain;
|
|
10
|
+
background-color: transparent;
|
|
11
|
+
border-radius:3px;
|
|
12
|
+
--apx-tristate-tick-color: transparent;
|
|
13
|
+
--apx-tristate-checked-color: #0654ba;
|
|
14
|
+
--apx-tristate-crossed-color: #f00;
|
|
15
|
+
}
|
|
16
|
+
.apx-tristate.unchecked {
|
|
17
|
+
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cmask id='frame-cutout'%3E%3Crect width='32' height='32' fill='%23fff'/%3E%3Crect x='3' y='3' width='26' height='26' rx='4' ry='4' fill='%23000'/%3E%3C/mask%3E%3Crect width='32' height='32' fill='%23cccccc' mask='url(%23frame-cutout)'/%3E%3C/svg%3E");
|
|
18
|
+
}
|
|
19
|
+
.apx-tristate.unchecked.apx-tristate--unchecked-indeterminate::after {
|
|
20
|
+
content: '';
|
|
21
|
+
position: absolute;
|
|
22
|
+
inset: 0;
|
|
23
|
+
display: block;
|
|
24
|
+
pointer-events: none;
|
|
25
|
+
}
|
|
26
|
+
/* When --mixed is used (e.g. parent + sub-checkboxes), dash shows only when children are mixed */
|
|
27
|
+
.apx-tristate.unchecked.apx-tristate--indeterminate-dash:not(.apx-tristate--mixed)::before,
|
|
28
|
+
.apx-tristate.unchecked.apx-tristate--indeterminate-dash:not(.apx-tristate--mixed)::after {
|
|
29
|
+
display: none;
|
|
30
|
+
}
|
|
31
|
+
.apx-tristate.unchecked.apx-tristate--indeterminate-dash::before {
|
|
32
|
+
content: '';
|
|
33
|
+
position: absolute;
|
|
34
|
+
inset: 0;
|
|
35
|
+
display: block;
|
|
36
|
+
pointer-events: none;
|
|
37
|
+
background-color: var(--apx-tristate-checked-color);
|
|
38
|
+
border-radius: inherit;
|
|
39
|
+
}
|
|
40
|
+
.apx-tristate.unchecked.apx-tristate--indeterminate-dash::after {
|
|
41
|
+
background-color: var(--apx-tristate-tick-color);
|
|
42
|
+
-webkit-mask-repeat: no-repeat;
|
|
43
|
+
-webkit-mask-position: center;
|
|
44
|
+
-webkit-mask-size: 100% 100%;
|
|
45
|
+
-webkit-mask-mode: alpha;
|
|
46
|
+
mask-repeat: no-repeat;
|
|
47
|
+
mask-position: center;
|
|
48
|
+
mask-size: 100% 100%;
|
|
49
|
+
mask-mode: alpha;
|
|
50
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M8 16h16' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
51
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M8 16h16' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
52
|
+
}
|
|
53
|
+
.apx-tristate.unchecked.apx-tristate--indeterminate-filled-check::after {
|
|
54
|
+
background-color: var(--apx-tristate-checked-color);
|
|
55
|
+
border-radius: inherit;
|
|
56
|
+
}
|
|
57
|
+
.apx-tristate.checked {
|
|
58
|
+
background-image: none;
|
|
59
|
+
background-color: var(--apx-tristate-checked-color);
|
|
60
|
+
}
|
|
61
|
+
.apx-tristate.crossed {
|
|
62
|
+
background-image: none;
|
|
63
|
+
background-color: var(--apx-tristate-crossed-color);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.apx-tristate.checked::after,
|
|
67
|
+
.apx-tristate.crossed::after {
|
|
68
|
+
content: '';
|
|
69
|
+
position: absolute;
|
|
70
|
+
inset: 0;
|
|
71
|
+
display: block;
|
|
72
|
+
background-color: var(--apx-tristate-tick-color);
|
|
73
|
+
-webkit-mask-repeat: no-repeat;
|
|
74
|
+
-webkit-mask-position: center;
|
|
75
|
+
-webkit-mask-size: 100% 100%;
|
|
76
|
+
-webkit-mask-mode: alpha;
|
|
77
|
+
mask-repeat: no-repeat;
|
|
78
|
+
mask-position: center;
|
|
79
|
+
mask-size: 100% 100%;
|
|
80
|
+
mask-mode: alpha;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.apx-tristate.checked::after {
|
|
85
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M6 17L12 23L26 9' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
86
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M6 17L12 23L26 9' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.apx-tristate.crossed::after {
|
|
90
|
+
-webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M8 8L24 24M24 8L8 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
91
|
+
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32' viewBox='0 0 32 32'%3E%3Cpath d='M8 8L24 24M24 8L8 24' fill='none' stroke='%23fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
25
92
|
}
|