u2ui 1.6.0 → 1.8.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.
@@ -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]');
@@ -3,7 +3,7 @@ Prominent hints, alerts and facts. `<u2-alert open variant=info>...</u2-alert>`
3
3
 
4
4
  ## Features
5
5
 
6
- - Various variants for Success, Warn, Error, and Info alerts.
6
+ - Various variants for Success, Warning, Error, and Info alerts.
7
7
  - Dismissable alerts allow user to close them.
8
8
  - Support for icons within alerts.
9
9
  - Customizable actions through buttons in the alert.
@@ -34,7 +34,7 @@ u2-alert::part(icon) {
34
34
  ### Attributes
35
35
 
36
36
  - `variant`
37
- Determines the alert type (`success`, `warn`, `error`, `info`); sets default icon and ARIA role.
37
+ Determines the alert type (`success`, `warning`, `error`, `info`); sets default icon and ARIA role. `warn` still works, but is deprecated.
38
38
 
39
39
  - `dismissable`
40
40
  Shows a close button. Without this attribute the close button is hidden.
@@ -17,7 +17,7 @@ u2-alert:not([open]) {
17
17
  }
18
18
 
19
19
  u2-alert[variant=success] { --color: var(--color-green); }
20
- u2-alert[variant=warn] { --color: var(--color-yellow); }
20
+ u2-alert:is([variant=warning], [variant=warn]) { --color: var(--color-yellow); } /* warn: deprecated */
21
21
  u2-alert[variant=error] { --color: var(--color-red); }
22
22
  u2-alert[variant=info] { --color: var(--color-blue); }
23
23
 
package/el/alert/alert.js CHANGED
@@ -106,11 +106,11 @@ export default class U2Alert extends U2Element {
106
106
  attributeChangedCallback(name, oldValue, newValue) {
107
107
  if (oldValue === newValue) return;
108
108
  if (name==='variant') {
109
- const variant = this.getAttribute('variant');
109
+ const variant = variantOf(this);
110
110
  this.shadowRoot.getElementById('container').role = variantData[variant]?.role || 'status'; // better to set role to host?
111
111
  }
112
112
  if (name==='variant' || name==='icon') {
113
- const variant = this.getAttribute('variant');
113
+ const variant = variantOf(this);
114
114
  const icon = this.getAttribute('icon') || variantData[variant]?.icon || 'info';
115
115
  this.shadowRoot.querySelector('[name=icon]').innerHTML = `<u2-ico icon=${icon}></u2-ico>`;
116
116
  }
@@ -150,7 +150,7 @@ const variantData = {
150
150
  icon: 'check',
151
151
  role: 'status',
152
152
  },
153
- warn: {
153
+ warning: {
154
154
  icon: 'warning',
155
155
  role: 'alert',
156
156
  },
@@ -160,6 +160,16 @@ const variantData = {
160
160
  }
161
161
  };
162
162
 
163
+ /** The variant; "warn" is read as "warning" (deprecated, warned about once). */
164
+ let warned = false;
165
+ function variantOf(el) {
166
+ const variant = el.getAttribute('variant');
167
+ if (variant !== 'warn') return variant;
168
+ if (!warned) console.warn('u2-alert: variant "warn" is deprecated, use "warning"', el);
169
+ warned = true;
170
+ return 'warning';
171
+ }
172
+
163
173
  customElements.get('u2-alert') || customElements.define('u2-alert', U2Alert);
164
174
 
165
175
  function alert(message, variant, options={}) {
@@ -173,7 +183,7 @@ function alert(message, variant, options={}) {
173
183
  }
174
184
  export const info = (message, options) => alert(message, 'info', options);
175
185
  export const success = (message, options) => alert(message, 'success', options);
176
- export const warn = (message, options) => alert(message, 'warn', options);
186
+ export const warn = (message, options) => alert(message, 'warning', options);
177
187
  export const error = (message, options) => alert(message, 'error', options);
178
188
 
179
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "u2ui",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
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",