@jungherz-de/glasskit-elements 1.21.2 → 1.22.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/CHANGELOG.md +22 -1
- package/README.md +6 -12
- package/SKILL.md +38 -68
- package/dist/components/base.js +27 -0
- package/dist/components/glk-checkbox.js +3 -2
- package/dist/components/glk-input.js +78 -10
- package/dist/components/glk-radio.js +3 -2
- package/dist/components/glk-range.js +11 -3
- package/dist/components/glk-search.js +9 -3
- package/dist/components/glk-select.js +31 -8
- package/dist/components/glk-textarea.js +17 -4
- package/dist/components/glk-toggle.js +3 -2
- package/dist/glasskit-elements.esm.js +184 -36
- package/dist/glasskit-elements.js +184 -36
- package/dist/glasskit-elements.min.js +1 -1
- package/package.json +3 -3
- package/src/base.js +27 -0
- package/src/components/forms/glk-checkbox.js +3 -2
- package/src/components/forms/glk-input.js +78 -10
- package/src/components/forms/glk-radio.js +3 -2
- package/src/components/forms/glk-range.js +11 -3
- package/src/components/forms/glk-search.js +9 -3
- package/src/components/forms/glk-select.js +31 -8
- package/src/components/forms/glk-textarea.js +17 -4
- package/src/components/forms/glk-toggle.js +3 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jungherz-de/glasskit-elements",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"description": "Vanilla JavaScript Web Components wrapping the GlassKit CSS glassmorphism library",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://glasskit-elements.jungherz.com",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"vanilla-js"
|
|
50
50
|
],
|
|
51
51
|
"peerDependencies": {
|
|
52
|
-
"@jungherz-de/glasskit": ">=1.
|
|
52
|
+
"@jungherz-de/glasskit": ">=1.22.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@jungherz-de/glasskit": "^1.
|
|
55
|
+
"@jungherz-de/glasskit": "^1.22.0",
|
|
56
56
|
"@rollup/plugin-node-resolve": "^15.0.0",
|
|
57
57
|
"@rollup/plugin-terser": "^0.4.0",
|
|
58
58
|
"rollup": "^4.0.0"
|
package/src/base.js
CHANGED
|
@@ -225,6 +225,9 @@ export class GlkElement extends HTMLElement {
|
|
|
225
225
|
|
|
226
226
|
connectedCallback() {
|
|
227
227
|
if (!this._initialized) {
|
|
228
|
+
// Before _initialized: setters that set attributes must not reach
|
|
229
|
+
// onAttributeChanged yet — render() reads the attributes anyway.
|
|
230
|
+
this._upgradeProperties();
|
|
228
231
|
this._initialized = true;
|
|
229
232
|
|
|
230
233
|
// Create theme wrapper (display:contents makes it layout-transparent)
|
|
@@ -259,6 +262,30 @@ export class GlkElement extends HTMLElement {
|
|
|
259
262
|
}
|
|
260
263
|
}
|
|
261
264
|
|
|
265
|
+
/**
|
|
266
|
+
* A property set on the element before its class was defined — markup a
|
|
267
|
+
* framework fills before the bundle has loaded — lands on the instance
|
|
268
|
+
* and hides the class's accessor: the setter never ran, and the value was
|
|
269
|
+
* lost (a glk-select kept its first option, a glk-progress stayed at 0).
|
|
270
|
+
* Such a value is taken off the instance and set again through the
|
|
271
|
+
* setter, which keeps what it cannot apply before render().
|
|
272
|
+
*/
|
|
273
|
+
_upgradeProperties() {
|
|
274
|
+
for (const key of Object.keys(this)) {
|
|
275
|
+
let proto = Object.getPrototypeOf(this);
|
|
276
|
+
let setter = null;
|
|
277
|
+
while (proto && proto !== HTMLElement.prototype) {
|
|
278
|
+
const desc = Object.getOwnPropertyDescriptor(proto, key);
|
|
279
|
+
if (desc) { setter = desc.set; break; }
|
|
280
|
+
proto = Object.getPrototypeOf(proto);
|
|
281
|
+
}
|
|
282
|
+
if (!setter) continue;
|
|
283
|
+
const value = this[key];
|
|
284
|
+
delete this[key];
|
|
285
|
+
this[key] = value;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
262
289
|
disconnectedCallback() {
|
|
263
290
|
instances.delete(this);
|
|
264
291
|
this._lightDomObserver?.disconnect();
|
|
@@ -92,11 +92,12 @@ class GlkCheckbox extends GlkFormElement {
|
|
|
92
92
|
this._syncFormValue();
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
|
|
95
|
+
// Before render() the attribute carries the state: render() reads it.
|
|
96
|
+
get checked() { return this._input ? this._input.checked : this.getBoolAttr('checked'); }
|
|
96
97
|
set checked(v) {
|
|
97
98
|
if (this._input) this._input.checked = v;
|
|
98
99
|
this.setBoolAttr('checked', v);
|
|
99
|
-
this._syncFormValue();
|
|
100
|
+
if (this._input) this._syncFormValue();
|
|
100
101
|
}
|
|
101
102
|
|
|
102
103
|
get disabled() { return this.getBoolAttr('disabled'); }
|
|
@@ -6,11 +6,26 @@ import { GlkFormElement } from '../../base.js';
|
|
|
6
6
|
// 1.17.0 — before, a date field with min still offered every past day).
|
|
7
7
|
const FORWARDED = ['min', 'max', 'step', 'minlength', 'maxlength', 'pattern', 'autocomplete', 'inputmode'];
|
|
8
8
|
|
|
9
|
+
// Prefix and suffix (since 1.22.0): two slots inside the field's box, on
|
|
10
|
+
// GlassKit's .glass-input-wrap. An affix shows only while something is
|
|
11
|
+
// slotted into it, and its width is measured, so the text keeps clear of a
|
|
12
|
+
// unit of any length. GlassKit lets clicks through an affix to the field;
|
|
13
|
+
// slotted elements take them here instead — a button or a link inside
|
|
14
|
+
// works — and a click on slotted text or an icon still lands in the field,
|
|
15
|
+
// because the shadow root delegates focus to it.
|
|
16
|
+
const affixSheet = new CSSStyleSheet();
|
|
17
|
+
affixSheet.replaceSync(`
|
|
18
|
+
.glass-input-wrap__prefix ::slotted(*),
|
|
19
|
+
.glass-input-wrap__suffix ::slotted(*) { pointer-events: auto; }
|
|
20
|
+
`);
|
|
21
|
+
|
|
9
22
|
class GlkInput extends GlkFormElement {
|
|
10
23
|
static get observedAttributes() {
|
|
11
|
-
return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'name', 'value', 'required', ...FORWARDED];
|
|
24
|
+
return ['label', 'type', 'placeholder', 'error', 'hint', 'disabled', 'readonly', 'name', 'value', 'required', ...FORWARDED];
|
|
12
25
|
}
|
|
13
26
|
|
|
27
|
+
static get hostStyles() { return affixSheet; }
|
|
28
|
+
|
|
14
29
|
render() {
|
|
15
30
|
const group = this.createElement('div', ['glass-input-group']);
|
|
16
31
|
|
|
@@ -32,19 +47,28 @@ class GlkInput extends GlkFormElement {
|
|
|
32
47
|
const name = this.getAttribute('name');
|
|
33
48
|
if (name) this._input.setAttribute('name', name);
|
|
34
49
|
|
|
35
|
-
|
|
50
|
+
// A value set as a property before render() has waited for it.
|
|
51
|
+
const value = this._pendingValue ?? this.getAttribute('value');
|
|
52
|
+
this._pendingValue = undefined;
|
|
36
53
|
if (value) this._input.value = value;
|
|
37
54
|
|
|
38
55
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
56
|
+
if (this.getBoolAttr('readonly')) this._input.readOnly = true;
|
|
39
57
|
if (this.getBoolAttr('required')) this._input.required = true;
|
|
40
58
|
for (const attr of FORWARDED) this._forward(attr);
|
|
41
59
|
|
|
60
|
+
// The field's box: the input, a prefix and a suffix slot on top of it
|
|
61
|
+
this._box = this.createElement('div', ['glass-input-wrap'], { part: 'box' });
|
|
62
|
+
this._prefix = this._createAffix('prefix');
|
|
63
|
+
this._suffix = this._createAffix('suffix');
|
|
64
|
+
this._box.append(this._prefix, this._input, this._suffix);
|
|
65
|
+
|
|
42
66
|
// Hint — read out with the field as its description
|
|
43
67
|
this._hintEl = this.createElement('span', this._computeHintClasses(), { id: 'hint' });
|
|
44
68
|
this._hintEl.textContent = this.getAttribute('hint') || '';
|
|
45
69
|
|
|
46
70
|
group.appendChild(this._labelEl);
|
|
47
|
-
group.appendChild(this.
|
|
71
|
+
group.appendChild(this._box);
|
|
48
72
|
if (this.getAttribute('hint')) group.appendChild(this._hintEl);
|
|
49
73
|
|
|
50
74
|
this._group = group;
|
|
@@ -54,6 +78,12 @@ class GlkInput extends GlkFormElement {
|
|
|
54
78
|
this._syncFormValue();
|
|
55
79
|
}
|
|
56
80
|
|
|
81
|
+
_createAffix(name) {
|
|
82
|
+
const affix = this.createElement('span', [`glass-input-wrap__${name}`], { id: name, part: name, hidden: '' });
|
|
83
|
+
affix.appendChild(this.createElement('slot', [], { name }));
|
|
84
|
+
return affix;
|
|
85
|
+
}
|
|
86
|
+
|
|
57
87
|
setupEvents() {
|
|
58
88
|
this._onInput = () => {
|
|
59
89
|
this._syncFormValue();
|
|
@@ -66,11 +96,26 @@ class GlkInput extends GlkFormElement {
|
|
|
66
96
|
};
|
|
67
97
|
this._input.addEventListener('input', this._onInput);
|
|
68
98
|
this._input.addEventListener('change', this._onChangeNative);
|
|
99
|
+
|
|
100
|
+
// Affixes: shown while something is slotted, measured while shown.
|
|
101
|
+
this._onSlotChange = () => this._applyAffixes();
|
|
102
|
+
this._box.addEventListener('slotchange', this._onSlotChange);
|
|
103
|
+
this._affixObserver = new ResizeObserver(entries => {
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
const size = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
106
|
+
this._box.style.setProperty(`--gl-input-${entry.target.id}-size`, `${size}px`);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
this._affixObserver.observe(this._prefix);
|
|
110
|
+
this._affixObserver.observe(this._suffix);
|
|
111
|
+
this._applyAffixes();
|
|
69
112
|
}
|
|
70
113
|
|
|
71
114
|
teardownEvents() {
|
|
72
115
|
this._input?.removeEventListener('input', this._onInput);
|
|
73
116
|
this._input?.removeEventListener('change', this._onChangeNative);
|
|
117
|
+
this._box?.removeEventListener('slotchange', this._onSlotChange);
|
|
118
|
+
this._affixObserver?.disconnect();
|
|
74
119
|
}
|
|
75
120
|
|
|
76
121
|
onAttributeChanged(name) {
|
|
@@ -100,6 +145,9 @@ class GlkInput extends GlkFormElement {
|
|
|
100
145
|
case 'disabled':
|
|
101
146
|
this._input.disabled = this.getBoolAttr('disabled');
|
|
102
147
|
break;
|
|
148
|
+
case 'readonly':
|
|
149
|
+
this._input.readOnly = this.getBoolAttr('readonly');
|
|
150
|
+
break;
|
|
103
151
|
case 'name':
|
|
104
152
|
this._input.setAttribute('name', this.getAttribute('name') || '');
|
|
105
153
|
break;
|
|
@@ -115,14 +163,27 @@ class GlkInput extends GlkFormElement {
|
|
|
115
163
|
}
|
|
116
164
|
}
|
|
117
165
|
|
|
166
|
+
/** An affix shows, and makes room in the field, only while something is slotted. */
|
|
167
|
+
_applyAffixes() {
|
|
168
|
+
for (const affix of [this._prefix, this._suffix]) {
|
|
169
|
+
const filled = affix.firstElementChild.assignedNodes().length > 0;
|
|
170
|
+
affix.hidden = !filled;
|
|
171
|
+
this._box.classList.toggle(`glass-input-wrap--${affix.id}`, filled);
|
|
172
|
+
}
|
|
173
|
+
this._applyDescription();
|
|
174
|
+
}
|
|
175
|
+
|
|
118
176
|
/**
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
177
|
+
* Prefix, suffix and hint describe the field — a unit belongs to the value
|
|
178
|
+
* it stands next to. With `error` set the hint is the error message and the
|
|
179
|
+
* field is marked invalid. Server-side errors arrive this way, so `error`
|
|
180
|
+
* stays a marker for assistive technology and does not block the form —
|
|
181
|
+
* only the field's own constraints (required, pattern …) do.
|
|
123
182
|
*/
|
|
124
183
|
_applyDescription() {
|
|
125
|
-
|
|
184
|
+
const ids = [this._prefix, this._suffix].filter(affix => affix && !affix.hidden).map(affix => affix.id);
|
|
185
|
+
if (this.getAttribute('hint')) ids.push('hint');
|
|
186
|
+
if (ids.length) this._input.setAttribute('aria-describedby', ids.join(' '));
|
|
126
187
|
else this._input.removeAttribute('aria-describedby');
|
|
127
188
|
if (this.getBoolAttr('error')) this._input.setAttribute('aria-invalid', 'true');
|
|
128
189
|
else this._input.removeAttribute('aria-invalid');
|
|
@@ -157,15 +218,22 @@ class GlkInput extends GlkFormElement {
|
|
|
157
218
|
this._syncFormValue();
|
|
158
219
|
}
|
|
159
220
|
|
|
160
|
-
get value() { return this._input
|
|
221
|
+
get value() { return this._input ? this._input.value : (this._pendingValue ?? this.getAttribute('value') ?? ''); }
|
|
161
222
|
set value(v) {
|
|
162
|
-
|
|
223
|
+
const value = v == null ? '' : String(v);
|
|
224
|
+
// Frameworks set properties right after creating the element. Before
|
|
225
|
+
// render() the value waits for it — it used to throw.
|
|
226
|
+
if (!this._input) { this._pendingValue = value; return; }
|
|
227
|
+
this._input.value = value;
|
|
163
228
|
this._syncFormValue();
|
|
164
229
|
}
|
|
165
230
|
|
|
166
231
|
get disabled() { return this.getBoolAttr('disabled'); }
|
|
167
232
|
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
168
233
|
|
|
234
|
+
get readOnly() { return this.getBoolAttr('readonly'); }
|
|
235
|
+
set readOnly(v) { this.setBoolAttr('readonly', v); }
|
|
236
|
+
|
|
169
237
|
get name() { return this.getAttribute('name'); }
|
|
170
238
|
set name(v) { this.setAttribute('name', v); }
|
|
171
239
|
}
|
|
@@ -189,14 +189,15 @@ class GlkRadio extends GlkFormElement {
|
|
|
189
189
|
syncGroupTabIndex(this._group());
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
|
|
192
|
+
// Before render() the attribute carries the state: render() reads it.
|
|
193
|
+
get checked() { return this._input ? this._input.checked : this.getBoolAttr('checked'); }
|
|
193
194
|
set checked(v) {
|
|
194
195
|
if (this._input) this._input.checked = v;
|
|
195
196
|
// Also covers the case where the attribute is already present, so
|
|
196
197
|
// setBoolAttr stays silent and onAttributeChanged never runs.
|
|
197
198
|
if (v) this._uncheckPeers();
|
|
198
199
|
this.setBoolAttr('checked', v);
|
|
199
|
-
this._syncFormValue();
|
|
200
|
+
if (this._input) this._syncFormValue();
|
|
200
201
|
syncGroupTabIndex(this._group());
|
|
201
202
|
}
|
|
202
203
|
|
|
@@ -35,13 +35,17 @@ class GlkRange extends GlkFormElement {
|
|
|
35
35
|
|
|
36
36
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
37
37
|
|
|
38
|
+
// The reset value is the attribute's; a value set as a property before
|
|
39
|
+
// render() has waited for it and comes on top.
|
|
40
|
+
this._defaultValue = this._input.value;
|
|
41
|
+
if (this._pendingValue !== undefined) this._input.value = this._pendingValue;
|
|
42
|
+
this._pendingValue = undefined;
|
|
38
43
|
this._updateValueDisplay();
|
|
39
44
|
|
|
40
45
|
group.appendChild(header);
|
|
41
46
|
group.appendChild(this._input);
|
|
42
47
|
|
|
43
48
|
this._wrapper.appendChild(group);
|
|
44
|
-
this._defaultValue = this._input.value;
|
|
45
49
|
this._syncFormValue();
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -102,9 +106,13 @@ class GlkRange extends GlkFormElement {
|
|
|
102
106
|
this._syncFormValue();
|
|
103
107
|
}
|
|
104
108
|
|
|
105
|
-
get value() { return this._input
|
|
109
|
+
get value() { return this._input ? this._input.value : (this._pendingValue ?? this.getAttribute('value') ?? '50'); }
|
|
106
110
|
set value(v) {
|
|
107
|
-
|
|
111
|
+
const value = v == null ? '' : String(v);
|
|
112
|
+
// Frameworks set properties right after creating the element. Before
|
|
113
|
+
// render() the value waits for it — it used to throw.
|
|
114
|
+
if (!this._input) { this._pendingValue = value; return; }
|
|
115
|
+
this._input.value = value;
|
|
108
116
|
this._updateValueDisplay();
|
|
109
117
|
this._syncFormValue();
|
|
110
118
|
}
|
|
@@ -27,7 +27,9 @@ class GlkSearch extends GlkFormElement {
|
|
|
27
27
|
const name = this.getAttribute('name');
|
|
28
28
|
if (name) this._input.setAttribute('name', name);
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
// A value set as a property before render() has waited for it.
|
|
31
|
+
const value = this._pendingValue ?? this.getAttribute('value');
|
|
32
|
+
this._pendingValue = undefined;
|
|
31
33
|
if (value) this._input.value = value;
|
|
32
34
|
|
|
33
35
|
if (this.getBoolAttr('disabled')) this._input.disabled = true;
|
|
@@ -89,9 +91,13 @@ class GlkSearch extends GlkFormElement {
|
|
|
89
91
|
this._syncFormValue();
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
get value() { return this._input
|
|
94
|
+
get value() { return this._input ? this._input.value : (this._pendingValue ?? this.getAttribute('value') ?? ''); }
|
|
93
95
|
set value(v) {
|
|
94
|
-
|
|
96
|
+
const value = v == null ? '' : String(v);
|
|
97
|
+
// Frameworks set properties right after creating the element. Before
|
|
98
|
+
// render() the value waits for it — it used to throw.
|
|
99
|
+
if (!this._input) { this._pendingValue = value; return; }
|
|
100
|
+
this._input.value = value;
|
|
95
101
|
this._syncFormValue();
|
|
96
102
|
}
|
|
97
103
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import { GlkFormElement } from '../../base.js';
|
|
2
2
|
|
|
3
|
+
// The wanted value. A select can show a value only once an option carries it,
|
|
4
|
+
// and the options arrive late: they are copied in a frame after connecting,
|
|
5
|
+
// and a framework may add them later still — while it sets `value` as a
|
|
6
|
+
// property right after creating the element. So the value the attribute or
|
|
7
|
+
// the property asks for is kept as wanted until an option carries it; a
|
|
8
|
+
// choice of the user replaces it. Before 1.22.0 only the attribute was kept:
|
|
9
|
+
// a property set before connecting threw, and one set before the options
|
|
10
|
+
// were copied was lost — the select showed its first option while the app
|
|
11
|
+
// held the other value.
|
|
12
|
+
|
|
3
13
|
class GlkSelect extends GlkFormElement {
|
|
4
14
|
static get observedAttributes() {
|
|
5
15
|
return ['label', 'disabled', 'name', 'value', 'required'];
|
|
@@ -15,6 +25,8 @@ class GlkSelect extends GlkFormElement {
|
|
|
15
25
|
this._labelEl.textContent = this.getAttribute('label') || '';
|
|
16
26
|
|
|
17
27
|
this._select = this.createElement('select', ['glass-select'], { id: 'field' });
|
|
28
|
+
// A value set as a property before render() is wanted already.
|
|
29
|
+
if (this._wanted === undefined) this._wanted = this.getAttribute('value');
|
|
18
30
|
|
|
19
31
|
const name = this.getAttribute('name');
|
|
20
32
|
if (name) this._select.setAttribute('name', name);
|
|
@@ -46,10 +58,10 @@ class GlkSelect extends GlkFormElement {
|
|
|
46
58
|
|
|
47
59
|
this._moveOptions();
|
|
48
60
|
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
if (!this._applyValue(
|
|
61
|
+
// The wanted value first, then the live selection if it survived the
|
|
62
|
+
// rebuild — without that, the selection would jump back to the first entry
|
|
63
|
+
// every time the list is updated.
|
|
64
|
+
if (!this._applyValue(this._wanted)) this._applyValue(previous);
|
|
53
65
|
this._syncFormValue();
|
|
54
66
|
}
|
|
55
67
|
|
|
@@ -75,6 +87,7 @@ class GlkSelect extends GlkFormElement {
|
|
|
75
87
|
|
|
76
88
|
setupEvents() {
|
|
77
89
|
this._onChange = () => {
|
|
90
|
+
this._wanted = null; // the user's choice replaces it
|
|
78
91
|
this._syncFormValue();
|
|
79
92
|
this.emit('glk-change', { value: this._select.value });
|
|
80
93
|
this.dispatchEvent(new Event('change', { bubbles: true }));
|
|
@@ -99,7 +112,8 @@ class GlkSelect extends GlkFormElement {
|
|
|
99
112
|
this._select.setAttribute('name', this.getAttribute('name') || '');
|
|
100
113
|
break;
|
|
101
114
|
case 'value':
|
|
102
|
-
this.
|
|
115
|
+
this._wanted = this.getAttribute('value');
|
|
116
|
+
this._applyValue(this._wanted);
|
|
103
117
|
this._syncFormValue();
|
|
104
118
|
break;
|
|
105
119
|
case 'required':
|
|
@@ -115,13 +129,22 @@ class GlkSelect extends GlkFormElement {
|
|
|
115
129
|
}
|
|
116
130
|
|
|
117
131
|
resetValue() {
|
|
118
|
-
|
|
132
|
+
// Back to what the markup asked for, else the first option.
|
|
133
|
+
this._wanted = this.getAttribute('value');
|
|
134
|
+
if (!this._applyValue(this._wanted)) this._select.selectedIndex = 0;
|
|
119
135
|
this._syncFormValue();
|
|
120
136
|
}
|
|
121
137
|
|
|
122
|
-
get value() {
|
|
138
|
+
get value() {
|
|
139
|
+
// Until options are there, the wanted value is the answer: a framework
|
|
140
|
+
// reads back what it has just set.
|
|
141
|
+
if (!this._select?.options.length) return this._wanted ?? this.getAttribute('value') ?? '';
|
|
142
|
+
return this._select.value;
|
|
143
|
+
}
|
|
123
144
|
set value(v) {
|
|
124
|
-
|
|
145
|
+
this._wanted = v == null ? '' : String(v);
|
|
146
|
+
if (!this._select) return; // render() takes it from here
|
|
147
|
+
this._applyValue(this._wanted);
|
|
125
148
|
this._syncFormValue();
|
|
126
149
|
}
|
|
127
150
|
|
|
@@ -2,7 +2,7 @@ import { GlkFormElement } from '../../base.js';
|
|
|
2
2
|
|
|
3
3
|
class GlkTextarea extends GlkFormElement {
|
|
4
4
|
static get observedAttributes() {
|
|
5
|
-
return ['label', 'placeholder', 'rows', 'disabled', 'name', 'value', 'required'];
|
|
5
|
+
return ['label', 'placeholder', 'rows', 'disabled', 'readonly', 'name', 'value', 'required'];
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
render() {
|
|
@@ -22,10 +22,13 @@ class GlkTextarea extends GlkFormElement {
|
|
|
22
22
|
const name = this.getAttribute('name');
|
|
23
23
|
if (name) this._textarea.setAttribute('name', name);
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
// A value set as a property before render() has waited for it.
|
|
26
|
+
const value = this._pendingValue ?? this.getAttribute('value');
|
|
27
|
+
this._pendingValue = undefined;
|
|
26
28
|
if (value) this._textarea.value = value;
|
|
27
29
|
|
|
28
30
|
if (this.getBoolAttr('disabled')) this._textarea.disabled = true;
|
|
31
|
+
if (this.getBoolAttr('readonly')) this._textarea.readOnly = true;
|
|
29
32
|
if (this.getBoolAttr('required')) this._textarea.required = true;
|
|
30
33
|
|
|
31
34
|
group.appendChild(this._labelEl);
|
|
@@ -63,6 +66,9 @@ class GlkTextarea extends GlkFormElement {
|
|
|
63
66
|
case 'disabled':
|
|
64
67
|
this._textarea.disabled = this.getBoolAttr('disabled');
|
|
65
68
|
break;
|
|
69
|
+
case 'readonly':
|
|
70
|
+
this._textarea.readOnly = this.getBoolAttr('readonly');
|
|
71
|
+
break;
|
|
66
72
|
case 'name':
|
|
67
73
|
this._textarea.setAttribute('name', this.getAttribute('name') || '');
|
|
68
74
|
break;
|
|
@@ -87,14 +93,21 @@ class GlkTextarea extends GlkFormElement {
|
|
|
87
93
|
this._syncFormValue();
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
get value() { return this._textarea
|
|
96
|
+
get value() { return this._textarea ? this._textarea.value : (this._pendingValue ?? this.getAttribute('value') ?? ''); }
|
|
91
97
|
set value(v) {
|
|
92
|
-
|
|
98
|
+
const value = v == null ? '' : String(v);
|
|
99
|
+
// Frameworks set properties right after creating the element. Before
|
|
100
|
+
// render() the value waits for it — it used to throw.
|
|
101
|
+
if (!this._textarea) { this._pendingValue = value; return; }
|
|
102
|
+
this._textarea.value = value;
|
|
93
103
|
this._syncFormValue();
|
|
94
104
|
}
|
|
95
105
|
|
|
96
106
|
get disabled() { return this.getBoolAttr('disabled'); }
|
|
97
107
|
set disabled(v) { this.setBoolAttr('disabled', v); }
|
|
108
|
+
|
|
109
|
+
get readOnly() { return this.getBoolAttr('readonly'); }
|
|
110
|
+
set readOnly(v) { this.setBoolAttr('readonly', v); }
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
customElements.define('glk-textarea', GlkTextarea);
|
|
@@ -104,11 +104,12 @@ class GlkToggle extends GlkFormElement {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
|
|
107
|
+
// Before render() the attribute carries the state: render() reads it.
|
|
108
|
+
get checked() { return this._input ? this._input.checked : this.getBoolAttr('checked'); }
|
|
108
109
|
set checked(v) {
|
|
109
110
|
if (this._input) this._input.checked = v;
|
|
110
111
|
this.setBoolAttr('checked', v);
|
|
111
|
-
this._syncFormValue();
|
|
112
|
+
if (this._input) this._syncFormValue();
|
|
112
113
|
}
|
|
113
114
|
|
|
114
115
|
get disabled() { return this.getBoolAttr('disabled'); }
|