u2ui 1.6.0 → 1.7.5

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.
@@ -19,6 +19,26 @@ Disable form-elements (like inputs, buttons, fieldsets) based on a condition
19
19
  }
20
20
  ```
21
21
 
22
+ ## Conditions
23
+
24
+ | condition | disabled while |
25
+ |---|---|
26
+ | `name` | a value of `name` is not empty |
27
+ | `!name` | no value of `name` is not empty |
28
+ | `name=v` | a value equals `v` |
29
+ | `name!=v` | no value equals `v` |
30
+ | `name>n`, `name<n` | a value is a number greater / less than `n` |
31
+
32
+ A field's values are what the form would send under its name (`FormData`): none for an unchecked
33
+ checkbox or a disabled field, several for same-named fields or a `select multiple`, a file by its name.
34
+ So a field disabled by its own condition counts as empty for the conditions that depend on it, and
35
+ chains settle in one go. Each check builds the `FormData`, which fires the form's `formdata` event.
36
+ A field that is not in the form counts as empty, with a warning (a typo?).
37
+
38
+ Typing applies at once; fields and conditions added, removed or changed in the form apply on the
39
+ next microtask. A value set by script (`input.value = …`) is only seen with an `input` event.
40
+ Removing the attribute leaves the element as it is.
41
+
22
42
  ## Install
23
43
 
24
44
  ```html
@@ -29,6 +49,7 @@ Disable form-elements (like inputs, buttons, fieldsets) based on a condition
29
49
 
30
50
  [test.html](http://gcdn.li/u2ui/u2@main/attr/disableif/tests/test.html)
31
51
  [minimal.html](http://gcdn.li/u2ui/u2@main/attr/disableif/tests/minimal.html)
52
+ [disableif.test.html](http://gcdn.li/u2ui/u2@main/attr/disableif/tests/disableif.test.html) (tests)
32
53
 
33
54
  ## About
34
55
 
@@ -1,59 +1,86 @@
1
1
  import { SelectorObserver } from '../../js/SelectorObserver/SelectorObserver.js';
2
2
 
3
- function checkCondition(el) {
4
- const condition = el.getAttribute('u2-disableif');
5
-
6
- const match = condition.match(/^(!?)([^=!<>]*)(=|!=|>|<)?([^=!<>]*)$/);
7
- if (match) {
8
- let [_, not, field, operator, value] = match;
9
- if (not) {
10
- if (operator) console.warn(`u2-disableif: "!" operator is not supported with other operators`, { el, field, operator, value });
11
- operator = '=';
12
- }
13
- const target = el.form.elements[field];
14
- if (!target) {
15
- console.warn(`u2-disableif: field "${field}" not found`, { el, field, operator, value });
16
- return;
17
- }
18
- let stringTargetValue = target.value;
19
- if (target.type === 'checkbox') stringTargetValue = target.checked ? target.value : '';
3
+ /** Once per element and problem, not on every update. */
4
+ const warned = new WeakMap();
5
+ function warn(el, problem) {
6
+ if (warned.get(el) === problem) return;
7
+ warned.set(el, problem);
8
+ console.warn('u2-disableif: ' + problem, el);
9
+ }
20
10
 
21
- const numericTargetValue = parseFloat(stringTargetValue);
22
- const numericConditionValue = parseFloat(value);
11
+ /**
12
+ * Whether the element's condition holds. A field's values are what the form would send under its
13
+ * name (FormData): none for an unchecked box or a disabled field, several for same-named fields or
14
+ * a select multiple; a file counts by its name. undefined: no valid condition (yet).
15
+ */
16
+ function holds(el, data) {
17
+ const match = el.getAttribute('u2-disableif').match(/^(!?)([^=!<>]*)(=|!=|>|<)?([^=!<>]*)$/);
18
+ if (!match) return;
19
+ const [, not, field, operator, value] = match;
20
+ const form = el.form;
21
+ // a custom element that is not defined yet is no form field yet: check again once it is
22
+ const pending = !form.elements[field] && form.querySelector(`:not(:defined)[name="${CSS.escape(field)}"]`);
23
+ if (pending) { customElements.whenDefined(pending.localName).then(() => schedule(form)); return; }
24
+ if (not && operator) { warn(el, `"!" cannot be combined with "${operator}"`); return; }
25
+ if (!form.elements[field]) warn(el, `field "${field}" not found, so it is empty`); // a typo?
26
+ const values = data.getAll(field).map(v => typeof v === 'string' ? v : v.name);
27
+ switch (operator) {
28
+ case '=': return values.includes(value);
29
+ case '!=': return !values.includes(value);
30
+ case '>': return values.some(v => parseFloat(v) > parseFloat(value));
31
+ case '<': return values.some(v => parseFloat(v) < parseFloat(value));
32
+ }
33
+ return values.some(Boolean) !== !!not;
34
+ }
23
35
 
24
- let conditionMet = false;
25
- switch (operator) {
26
- case undefined:
27
- conditionMet = !!stringTargetValue;
28
- break;
29
- case '=':
30
- conditionMet = stringTargetValue === value;
31
- break;
32
- case '!=':
33
- conditionMet = stringTargetValue !== value;
34
- break;
35
- case '>':
36
- conditionMet = numericTargetValue > numericConditionValue;
37
- break;
38
- case '<':
39
- conditionMet = numericTargetValue < numericConditionValue;
40
- break;
36
+ /** Applies all conditions of the form, again until nothing changes: disabling a field empties it for
37
+ * the conditions that depend on it. A cycle stops after as many passes as there are conditions. */
38
+ function update(form) {
39
+ const els = [...form.elements].filter(el => el.hasAttribute('u2-disableif'));
40
+ for (let pass = 0; pass <= els.length; pass++) {
41
+ const data = new FormData(form); // fires "formdata", once per pass
42
+ let changed = false;
43
+ for (const el of els) {
44
+ const disabled = holds(el, data);
45
+ if (disabled === undefined || disabled === el.disabled) continue;
46
+ el.disabled = disabled;
47
+ changed = true;
41
48
  }
42
- el.disabled = conditionMet;
49
+ if (!changed) return;
43
50
  }
44
51
  }
45
52
 
53
+ /** One update per form and `wait` (microtask, frame), however many changes ask for it. */
54
+ const scheduled = new Map();
55
+ function schedule(form, wait = queueMicrotask) {
56
+ const forms = scheduled.get(wait) ?? scheduled.set(wait, new WeakSet()).get(wait);
57
+ if (!form || forms.has(form)) return;
58
+ forms.add(form);
59
+ wait(() => { forms.delete(form); update(form); });
60
+ }
61
+
62
+ /** Typing applies at once. Fields and conditions that are added, removed or changed in the form
63
+ * apply on the next microtask; one that changes its form also updates the new one. */
64
+ const ATTRIBUTES = ['u2-disableif', 'name', 'form', 'value', 'checked', 'selected', 'multiple', 'type'];
65
+ const watched = new WeakSet();
66
+ function watch(form) {
67
+ if (!form || watched.has(form)) return;
68
+ watched.add(form);
69
+ form.addEventListener('input', () => update(form));
70
+ form.addEventListener('reset', () => setTimeout(() => update(form))); // the values change after the event
71
+ new MutationObserver(records => {
72
+ schedule(form);
73
+ for (const { target } of records) if (target.form !== form) { watch(target.form); schedule(target.form); }
74
+ }).observe(form, { subtree: true, childList: true, attributes: true, attributeFilter: ATTRIBUTES });
75
+ }
46
76
 
47
77
  new SelectorObserver({
48
78
  on: (el) => {
49
- checkCondition(el);
79
+ watch(el.form);
80
+ schedule(el.form);
50
81
  // wait for custom-element-inputs
51
- // queueMicrotask(()=>checkCondition(el));
52
- requestAnimationFrame(()=>checkCondition(el));
53
- //setTimeout(()=>checkCondition(el));
54
- const form = el.form;
55
- el.form.addEventListener('input', () => checkCondition(el));
82
+ // queueMicrotask(() => schedule(el.form));
83
+ schedule(el.form, requestAnimationFrame);
84
+ // setTimeout(() => schedule(el.form));
56
85
  },
57
- off: (el) => {
58
- }
59
86
  }).observe('[u2-disableif]');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u2ui",
3
- "version": "1.6.0",
3
+ "version": "1.7.5",
4
4
  "description": "HTML Advanced: More Capabilities, Same Feel, Same Simplicity",
5
5
  "keywords": ["web-components","pattern-library","component-library","ui-toolkit","design-system"],
6
6
  "author": "Tobias Buschor",