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.
- package/attr/disableif/README.md +21 -0
- package/attr/disableif/disableif.js +72 -45
- package/el/alert/README.md +2 -2
- package/el/alert/alert.css +1 -1
- package/el/alert/alert.js +14 -4
- package/package.json +1 -1
package/attr/disableif/README.md
CHANGED
|
@@ -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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
watch(el.form);
|
|
80
|
+
schedule(el.form);
|
|
50
81
|
// wait for custom-element-inputs
|
|
51
|
-
// queueMicrotask(()=>
|
|
52
|
-
|
|
53
|
-
//setTimeout(()=>
|
|
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/el/alert/README.md
CHANGED
|
@@ -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,
|
|
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`, `
|
|
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.
|
package/el/alert/alert.css
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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, '
|
|
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.
|
|
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",
|