@zeyos/zx 4.4.0 → 5.2.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 +447 -0
- package/README.md +44 -11
- package/dist/types/components/account-menu/account-menu.d.ts +8 -0
- package/dist/types/components/activity-list/activity-list.d.ts +31 -1
- package/dist/types/components/app-icon/app-icon.d.ts +14 -2
- package/dist/types/components/app-sidebar/app-sidebar.d.ts +293 -17
- package/dist/types/components/avatar/avatar.d.ts +46 -7
- package/dist/types/components/badge/badge.d.ts +21 -1
- package/dist/types/components/button/button.d.ts +15 -0
- package/dist/types/components/calendar/calendar.d.ts +67 -5
- package/dist/types/components/card-view/card-view.d.ts +7 -4
- package/dist/types/components/card-view/record-card.d.ts +11 -0
- package/dist/types/components/chart/chart.d.ts +10 -0
- package/dist/types/components/checklist/checklist.d.ts +9 -0
- package/dist/types/components/code-editor/code-editor.d.ts +6 -17
- package/dist/types/components/context-menu/context-menu.d.ts +6 -2
- package/dist/types/components/data-filter/data-filter.d.ts +4 -4
- package/dist/types/components/datebox/date-range-box.d.ts +5 -1
- package/dist/types/components/datebox/datebox.d.ts +7 -2
- package/dist/types/components/dialog/dialog.d.ts +46 -6
- package/dist/types/components/dropdown/dropdown.d.ts +24 -9
- package/dist/types/components/entity-ref/entity-ref.d.ts +2 -2
- package/dist/types/components/field/field.d.ts +54 -2
- package/dist/types/components/fieldset/external-adapter.d.ts +83 -0
- package/dist/types/components/fieldset/fieldset.d.ts +26 -1
- package/dist/types/components/file-item/file-item.d.ts +34 -6
- package/dist/types/components/file-list/file-list.d.ts +35 -3
- package/dist/types/components/filter/filter.d.ts +127 -3
- package/dist/types/components/filter-panel/filter-panel.d.ts +36 -7
- package/dist/types/components/form/form.d.ts +12 -1
- package/dist/types/components/kanban-view/kanban-card.d.ts +8 -0
- package/dist/types/components/launcher/launcher.d.ts +268 -15
- package/dist/types/components/list-toolbar/list-toolbar.d.ts +16 -7
- package/dist/types/components/menu-button/menu-button.d.ts +39 -5
- package/dist/types/components/message/message.d.ts +132 -17
- package/dist/types/components/modal/modal.d.ts +43 -2
- package/dist/types/components/module-bar/module-bar.d.ts +395 -0
- package/dist/types/components/pagination/pagination.d.ts +17 -3
- package/dist/types/components/select/color.d.ts +12 -0
- package/dist/types/components/select/glyphs.d.ts +124 -0
- package/dist/types/components/select/select.d.ts +17 -3
- package/dist/types/components/sheet/sheet.d.ts +15 -0
- package/dist/types/components/stat-tile/stat-tile.d.ts +16 -5
- package/dist/types/components/tabbox/tabbox.d.ts +9 -0
- package/dist/types/components/table/table.d.ts +311 -18
- package/dist/types/components/tag-picker/tag-picker.d.ts +21 -4
- package/dist/types/components/tree/tree.d.ts +13 -0
- package/dist/types/components/view/record-view.d.ts +59 -2
- package/dist/types/components/view-settings/view-settings.d.ts +879 -0
- package/dist/types/components/workspace-switcher/workspace-switcher.d.ts +327 -0
- package/dist/types/core/breakpoint.d.ts +1 -1
- package/dist/types/core/component.d.ts +12 -2
- package/dist/types/core/i18n.d.ts +27 -3
- package/dist/types/core/icons.d.ts +23 -25
- package/dist/types/core/keyboard.d.ts +98 -1
- package/dist/types/core/messages.d.ts +382 -0
- package/dist/types/core/motion.d.ts +59 -0
- package/dist/types/core/overlay-host.d.ts +42 -1
- package/dist/types/core/overlay-stack.d.ts +260 -0
- package/dist/types/index.d.ts +10 -5
- package/dist/types/internal/app-rail.d.ts +38 -0
- package/dist/types/internal/menu-items.d.ts +71 -3
- package/dist/types/zeyos/connect.d.ts +6 -0
- package/dist/types/zeyos/form.d.ts +50 -9
- package/dist/types/zeyos/index.d.ts +2 -1
- package/dist/types/zeyos/launcher.d.ts +9 -0
- package/dist/types/zeyos/module-bar.d.ts +176 -0
- package/dist/types/zeyos/query.d.ts +110 -5
- package/dist/types/zeyos/schema.d.ts +24 -1
- package/dist/types/zeyos/table.d.ts +345 -22
- package/dist/types/zeyos/view.d.ts +26 -2
- package/dist/zx-zeyos.esm.js +6178 -1008
- package/dist/zx-zeyos.esm.min.js +53 -5
- package/dist/zx.css +2861 -827
- package/dist/zx.esm.js +8909 -1480
- package/dist/zx.esm.min.js +55 -7
- package/dist/zx.global.js +8911 -1482
- package/dist/zx.global.min.js +55 -7
- package/dist/zx.min.css +1 -1
- package/docs/api.json +1079 -117
- package/docs/llms.md +956 -154
- package/docs/llms.txt +9 -5
- package/package.json +1 -1
- package/src/components/account-menu/account-menu.css +35 -10
- package/src/components/account-menu/account-menu.js +12 -0
- package/src/components/activity-item/activity-item.css +1 -1
- package/src/components/activity-item/activity-item.js +3 -0
- package/src/components/activity-list/activity-list.css +38 -3
- package/src/components/activity-list/activity-list.js +61 -1
- package/src/components/app-icon/app-icon.css +14 -3
- package/src/components/app-icon/app-icon.js +11 -1
- package/src/components/app-rail/app-rail.css +59 -79
- package/src/components/app-sidebar/app-sidebar.css +304 -168
- package/src/components/app-sidebar/app-sidebar.js +667 -46
- package/src/components/aurora/aurora.css +5 -3
- package/src/components/avatar/avatar.css +32 -7
- package/src/components/avatar/avatar.js +72 -8
- package/src/components/badge/badge.css +54 -2
- package/src/components/badge/badge.js +29 -2
- package/src/components/breadcrumb/breadcrumb.css +1 -1
- package/src/components/button/button.css +67 -9
- package/src/components/button/button.js +38 -0
- package/src/components/calendar/calendar.css +47 -47
- package/src/components/calendar/calendar.js +26 -52
- package/src/components/card/card.css +4 -4
- package/src/components/card-view/card-view.css +14 -12
- package/src/components/card-view/card-view.js +13 -9
- package/src/components/card-view/record-card.js +14 -3
- package/src/components/chart/chart.css +4 -1
- package/src/components/chart/chart.js +12 -0
- package/src/components/check-button/check-button.css +10 -2
- package/src/components/checklist/checklist.css +9 -2
- package/src/components/checklist/checklist.js +11 -5
- package/src/components/checklist/field-adapter.js +3 -1
- package/src/components/code-editor/code-editor.css +7 -6
- package/src/components/code-editor/code-editor.js +19 -1
- package/src/components/code-editor/field-adapter.js +9 -2
- package/src/components/context-menu/context-menu.css +9 -4
- package/src/components/context-menu/context-menu.js +8 -2
- package/src/components/copy/copy-button.css +6 -0
- package/src/components/copy/copy-input.css +9 -4
- package/src/components/data-filter/data-filter.css +5 -4
- package/src/components/data-filter/data-filter.js +2 -2
- package/src/components/date-picker/date-picker.css +6 -6
- package/src/components/date-picker/date-range-picker.css +3 -3
- package/src/components/date-picker/time-picker.css +3 -3
- package/src/components/datebox/date-range-box.css +79 -2
- package/src/components/datebox/date-range-box.js +13 -2
- package/src/components/datebox/datebox.css +79 -2
- package/src/components/datebox/datebox.js +13 -2
- package/src/components/dialog/dialog.css +26 -0
- package/src/components/dialog/dialog.js +99 -17
- package/src/components/dock/dock.css +5 -4
- package/src/components/dropdown/dropdown.css +1 -1
- package/src/components/dropdown/dropdown.js +159 -72
- package/src/components/empty-state/empty-state.css +2 -2
- package/src/components/entity-ref/entity-ref.css +1 -1
- package/src/components/entity-ref/entity-ref.js +1 -1
- package/src/components/field/field.css +28 -9
- package/src/components/field/field.js +101 -29
- package/src/components/field-upload/field-upload.css +1 -1
- package/src/components/fieldset/external-adapter.js +363 -0
- package/src/components/fieldset/fieldset.css +5 -3
- package/src/components/fieldset/fieldset.js +38 -64
- package/src/components/file-item/file-item.js +144 -10
- package/src/components/file-list/file-list.js +144 -21
- package/src/components/filter/filter.css +86 -2
- package/src/components/filter/filter.js +307 -11
- package/src/components/filter-panel/filter-panel.css +1 -1
- package/src/components/filter-panel/filter-panel.js +149 -32
- package/src/components/finder/finder.css +16 -14
- package/src/components/form/form.css +5 -4
- package/src/components/form/form.js +11 -2
- package/src/components/grid/grid.css +1 -1
- package/src/components/groupbox/groupbox.css +1 -1
- package/src/components/kanban-view/kanban-card.js +11 -3
- package/src/components/kanban-view/kanban-view.css +42 -18
- package/src/components/kanban-view/kanban-view.js +9 -6
- package/src/components/launcher/launcher.css +108 -26
- package/src/components/launcher/launcher.js +530 -42
- package/src/components/list-toolbar/list-toolbar.css +13 -8
- package/src/components/list-toolbar/list-toolbar.js +40 -14
- package/src/components/master-panel/master-panel.css +2 -0
- package/src/components/master-panel/master-panel.js +0 -1
- package/src/components/menu-button/menu-button.css +72 -4
- package/src/components/menu-button/menu-button.js +160 -18
- package/src/components/message/message.css +33 -3
- package/src/components/message/message.js +264 -22
- package/src/components/modal/modal.css +1 -1
- package/src/components/modal/modal.js +83 -15
- package/src/components/module-bar/module-bar.css +486 -0
- package/src/components/module-bar/module-bar.js +1074 -0
- package/src/components/multi-value-editor/multi-value-editor.css +1 -1
- package/src/components/navigation-bar/navigation-bar.css +18 -9
- package/src/components/number-field/field-adapter.js +3 -1
- package/src/components/number-field/number-field.css +70 -0
- package/src/components/number-field/number-field.js +2 -0
- package/src/components/pagination/pagination.css +13 -11
- package/src/components/pagination/pagination.js +20 -4
- package/src/components/panel/panel.css +4 -3
- package/src/components/questionnaire/questionnaire.css +4 -4
- package/src/components/rating/field-adapter.js +2 -1
- package/src/components/rating/rating.css +8 -1
- package/src/components/search/search.css +4 -1
- package/src/components/select/color.js +22 -0
- package/src/components/select/field-adapter.js +2 -0
- package/src/components/select/glyphs.js +165 -0
- package/src/components/select/select.css +138 -45
- package/src/components/select/select.js +92 -47
- package/src/components/sheet/sheet.css +12 -9
- package/src/components/sheet/sheet.js +74 -32
- package/src/components/slider/field-adapter.js +6 -1
- package/src/components/slider/slider.css +18 -4
- package/src/components/slider/slider.js +1 -1
- package/src/components/sort-control/sort-control.css +4 -1
- package/src/components/split-view/split-view.css +1 -1
- package/src/components/stat-tile/stat-tile.css +42 -47
- package/src/components/stat-tile/stat-tile.js +17 -6
- package/src/components/stepper/stepper.css +1 -1
- package/src/components/tabbox/tabbox.css +41 -32
- package/src/components/tabbox/tabbox.js +5 -1
- package/src/components/table/table.css +187 -37
- package/src/components/table/table.js +718 -73
- package/src/components/tag-picker/field-adapter.js +2 -1
- package/src/components/tag-picker/tag-picker.css +83 -5
- package/src/components/tag-picker/tag-picker.js +25 -8
- package/src/components/timebox/timebox.css +22 -2
- package/src/components/toggle/toggle.css +12 -1
- package/src/components/toolbar/toolbar.js +2 -2
- package/src/components/tooltip/tooltip.css +4 -0
- package/src/components/tooltip/tooltip.js +44 -19
- package/src/components/tree/tree.css +26 -17
- package/src/components/tree/tree.js +31 -0
- package/src/components/value-list/value-list.css +3 -3
- package/src/components/view/record-view.css +1 -1
- package/src/components/view/record-view.js +178 -18
- package/src/components/view-settings/view-settings.css +285 -0
- package/src/components/view-settings/view-settings.js +1830 -0
- package/src/components/workspace-switcher/workspace-switcher.css +188 -0
- package/src/components/workspace-switcher/workspace-switcher.js +463 -0
- package/src/core/breakpoint.js +1 -1
- package/src/core/component.js +88 -17
- package/src/core/fontawesome.js +5 -1
- package/src/core/i18n.js +47 -4
- package/src/core/icons.js +204 -61
- package/src/core/keyboard.js +206 -11
- package/src/core/messages.js +427 -0
- package/src/core/motion.js +147 -0
- package/src/core/overlay-host.js +77 -3
- package/src/core/overlay-stack.js +487 -0
- package/src/core/position.js +22 -3
- package/src/index.js +10 -5
- package/src/internal/app-rail.js +135 -55
- package/src/internal/menu-items.js +61 -3
- package/src/zeyos/connect.js +6 -2
- package/src/zeyos/form.js +69 -27
- package/src/zeyos/index.js +4 -1
- package/src/zeyos/launcher.js +8 -3
- package/src/zeyos/module-bar.js +181 -0
- package/src/zeyos/query.js +197 -13
- package/src/zeyos/schema.js +144 -27
- package/src/zeyos/table.js +287 -84
- package/src/zeyos/view.js +49 -93
- package/styles/base.css +72 -1
- package/styles/icons.css +11 -1
- package/styles/print.css +1 -1
- package/styles/tokens/dark.css +117 -37
- package/styles/tokens/global.css +121 -27
- package/styles/tokens/semantic.css +140 -43
- package/styles/tokens/themes.css +7 -6
- package/styles/zx.css +3 -1
- package/styles/tokens/density.css +0 -9
|
@@ -4,7 +4,10 @@ import { uid } from '../../core/util.js';
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @typedef {Object} FieldOptions
|
|
7
|
-
* @property {string|null} [id=null]
|
|
7
|
+
* @property {string|null} [id=null] DOM id of the control. Generated when omitted; a Fieldset
|
|
8
|
+
* derives it from its `idPrefix` and the field key, so a key never becomes a DOM id by itself.
|
|
9
|
+
* @property {string|null} [name=null] Field name: the `name` of a native control and the key the
|
|
10
|
+
* field is filed under. A Fieldset sets it to the field key; a standalone field falls back to `id`.
|
|
8
11
|
* @property {string} [type='text'] Registered field type.
|
|
9
12
|
* @property {string} [label=''] Visible label.
|
|
10
13
|
* @property {string} [description=''] Supporting description.
|
|
@@ -12,6 +15,9 @@ import { uid } from '../../core/util.js';
|
|
|
12
15
|
* @property {string} [placeholder=''] Control placeholder.
|
|
13
16
|
* @property {boolean} [required=false] Whether a value is required.
|
|
14
17
|
* @property {boolean} [disabled=false] Whether the control is disabled.
|
|
18
|
+
* @property {boolean} [readonly=false] Whether the value is read-only: shown and focusable, not
|
|
19
|
+
* editable. Types whose control has no read-only state (`checkbox`, `select`, `optionlist`,
|
|
20
|
+
* `zxselect`, `date`, `time`, …) ignore it.
|
|
15
21
|
* @property {string[]|Record<string, string>|null} [options=null] Select or option-list values.
|
|
16
22
|
* @property {'stack'|'inline'} [layout='stack'] Label/control layout.
|
|
17
23
|
* @property {Record<string, unknown>} [props={}] Adapter-specific control properties.
|
|
@@ -27,7 +33,12 @@ import { uid } from '../../core/util.js';
|
|
|
27
33
|
* @property {(value: unknown, options?: {silent?: boolean}) => void} set Set the current value.
|
|
28
34
|
* @property {() => void} focus Focus the control.
|
|
29
35
|
* @property {(disabled: boolean) => void} setDisabled Set disabled state.
|
|
36
|
+
* @property {(readonly: boolean) => void} [setReadonly] Set read-only state; left out when the
|
|
37
|
+
* control has none.
|
|
30
38
|
* @property {() => void} [destroy] Release adapter-owned resources.
|
|
39
|
+
* @property {Element} [labelTarget] The element the field's label, description, and validity
|
|
40
|
+
* belong to when it is not `el` — the text input inside a composite control. Left out, a
|
|
41
|
+
* wrapper that cannot be labelled hands them to the first unnamed form control inside it.
|
|
31
42
|
*/
|
|
32
43
|
|
|
33
44
|
/** @typedef {(field: Field, options: Readonly<FieldOptions>) => FieldAdapter} FieldAdapterFactory */
|
|
@@ -36,6 +47,7 @@ import { uid } from '../../core/util.js';
|
|
|
36
47
|
/**
|
|
37
48
|
* One labelled form control backed by a registered adapter.
|
|
38
49
|
* @fires Field#change
|
|
50
|
+
* @fires Field#input
|
|
39
51
|
* @fires Field#invalid
|
|
40
52
|
* @extends {Component<FieldOptions>}
|
|
41
53
|
*/
|
|
@@ -45,6 +57,7 @@ export class Field extends Component {
|
|
|
45
57
|
/** @type {FieldOptions & {msg: Record<string, string>}} */
|
|
46
58
|
static defaults = {
|
|
47
59
|
id: null,
|
|
60
|
+
name: null,
|
|
48
61
|
type: 'text',
|
|
49
62
|
label: '',
|
|
50
63
|
description: '',
|
|
@@ -52,6 +65,7 @@ export class Field extends Component {
|
|
|
52
65
|
placeholder: '',
|
|
53
66
|
required: false,
|
|
54
67
|
disabled: false,
|
|
68
|
+
readonly: false,
|
|
55
69
|
options: null,
|
|
56
70
|
layout: 'stack',
|
|
57
71
|
props: {},
|
|
@@ -88,9 +102,10 @@ export class Field extends Component {
|
|
|
88
102
|
return Field.#adapters.has(String(type).trim().toLowerCase());
|
|
89
103
|
}
|
|
90
104
|
|
|
91
|
-
// `adapter` (FieldAdapter), `initialValue`,
|
|
92
|
-
//
|
|
93
|
-
// constructor (which calls render())
|
|
105
|
+
// `adapter` (FieldAdapter), `initialValue`, `controlId` (the control's DOM id), and `name` (the
|
|
106
|
+
// field name, '' when there is none) are assigned in render(); declaring them as class fields
|
|
107
|
+
// would re-initialize them to undefined after the base constructor (which calls render())
|
|
108
|
+
// returns — see AGENTS.md.
|
|
94
109
|
|
|
95
110
|
/** @returns {HTMLElement} */
|
|
96
111
|
render() {
|
|
@@ -100,11 +115,14 @@ export class Field extends Component {
|
|
|
100
115
|
const layout = this.options.layout === 'inline' ? 'inline' : 'stack';
|
|
101
116
|
this._ownedComponents = new Set();
|
|
102
117
|
this._ownedNodes = [];
|
|
103
|
-
this._rootAttributes = rememberAttributes(root, ['data-layout', 'data-field-type', 'data-disabled', 'data-state']);
|
|
118
|
+
this._rootAttributes = rememberAttributes(root, ['data-layout', 'data-field-type', 'data-disabled', 'data-readonly', 'data-state']);
|
|
104
119
|
root.dataset.layout = layout;
|
|
105
120
|
root.dataset.fieldType = type;
|
|
106
121
|
|
|
122
|
+
/** @type {string} The control's DOM id: `options.id`, or a generated one. */
|
|
107
123
|
this.controlId = this.options.id ? String(this.options.id) : uid('zx-field');
|
|
124
|
+
/** @type {string} The field name (`options.name`, else `options.id`), '' when there is none. */
|
|
125
|
+
this.name = String(this.options.name ?? this.options.id ?? '');
|
|
108
126
|
const labelId = `${this.controlId}-label`;
|
|
109
127
|
const descriptionId = `${this.controlId}-description`;
|
|
110
128
|
const highlightId = `${this.controlId}-highlight`;
|
|
@@ -148,9 +166,14 @@ export class Field extends Component {
|
|
|
148
166
|
|
|
149
167
|
const control = this.adapter.el;
|
|
150
168
|
if (!control.id) control.id = this.controlId;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
169
|
+
// A wrapper (a `custom` adapter's box) cannot carry a name; the control inside it can.
|
|
170
|
+
const labelled = this.adapter.labelTarget
|
|
171
|
+
?? (isLabelable(control) ? control : unnamedControlWithin(control)) ?? control;
|
|
172
|
+
this._labelled = labelled;
|
|
173
|
+
if (labelled !== control && isLabelable(labelled) && !labelled.id) labelled.id = `${this.controlId}-input`;
|
|
174
|
+
label.htmlFor = isLabelable(labelled) ? labelled.id : control.id;
|
|
175
|
+
if (!isLabelable(labelled)) labelled.setAttribute('aria-labelledby', labelId);
|
|
176
|
+
this._baseDescribedBy = labelled.getAttribute('aria-describedby') ?? '';
|
|
154
177
|
controlSlot.append(control);
|
|
155
178
|
this._descriptionId = String(this.options.description ?? '') ? descriptionId : '';
|
|
156
179
|
this._highlightId = highlightId;
|
|
@@ -159,6 +182,8 @@ export class Field extends Component {
|
|
|
159
182
|
this.initialValue = initialValue(type, this.options.value);
|
|
160
183
|
this.adapter.set(this.initialValue, { silent: true });
|
|
161
184
|
this.setDisabled(Boolean(this.options.disabled));
|
|
185
|
+
if (this.options.readonly) this.setReadonly(true);
|
|
186
|
+
// Adapters whose widget reports its own commits stop the native `change` before this sees it.
|
|
162
187
|
this.listen(control, 'change', () => {
|
|
163
188
|
this.emit('change', { value: this.getValue() });
|
|
164
189
|
});
|
|
@@ -217,6 +242,19 @@ export class Field extends Component {
|
|
|
217
242
|
return this;
|
|
218
243
|
}
|
|
219
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Makes the value read-only or editable again, where the control has a read-only state (see
|
|
247
|
+
* the `readonly` option); reflected as `data-readonly` on the root either way.
|
|
248
|
+
* @param {boolean} readonly Read-only state.
|
|
249
|
+
* @returns {this}
|
|
250
|
+
*/
|
|
251
|
+
setReadonly(readonly) {
|
|
252
|
+
const value = Boolean(readonly);
|
|
253
|
+
this.adapter.setReadonly?.(value);
|
|
254
|
+
this.el.dataset.readonly = String(value);
|
|
255
|
+
return this;
|
|
256
|
+
}
|
|
257
|
+
|
|
220
258
|
/**
|
|
221
259
|
* Displays a validation or status message.
|
|
222
260
|
* @param {string} message Message text.
|
|
@@ -231,11 +269,11 @@ export class Field extends Component {
|
|
|
231
269
|
this._highlight.hidden = !message;
|
|
232
270
|
if (normalizedKind === 'danger' && message) {
|
|
233
271
|
this._highlight.setAttribute('role', 'alert');
|
|
234
|
-
this.
|
|
272
|
+
this._labelled.setAttribute('aria-invalid', 'true');
|
|
235
273
|
this.emit('invalid', { message: String(message) });
|
|
236
274
|
} else {
|
|
237
275
|
this._highlight.removeAttribute('role');
|
|
238
|
-
this.
|
|
276
|
+
this._labelled.removeAttribute('aria-invalid');
|
|
239
277
|
}
|
|
240
278
|
this._syncDescription(Boolean(message));
|
|
241
279
|
return this;
|
|
@@ -250,7 +288,7 @@ export class Field extends Component {
|
|
|
250
288
|
this._highlight.textContent = '';
|
|
251
289
|
this._highlight.hidden = true;
|
|
252
290
|
this._highlight.removeAttribute('role');
|
|
253
|
-
this.
|
|
291
|
+
this._labelled.removeAttribute('aria-invalid');
|
|
254
292
|
this._syncDescription(false);
|
|
255
293
|
return this;
|
|
256
294
|
}
|
|
@@ -279,8 +317,8 @@ export class Field extends Component {
|
|
|
279
317
|
_syncDescription(highlighted = false) {
|
|
280
318
|
const ids = [this._baseDescribedBy, this._descriptionId, highlighted ? this._highlightId : '']
|
|
281
319
|
.flatMap((value) => value.split(/\s+/)).filter(Boolean);
|
|
282
|
-
if (ids.length) this.
|
|
283
|
-
else this.
|
|
320
|
+
if (ids.length) this._labelled.setAttribute('aria-describedby', [...new Set(ids)].join(' '));
|
|
321
|
+
else this._labelled.removeAttribute('aria-describedby');
|
|
284
322
|
}
|
|
285
323
|
|
|
286
324
|
/** @returns {void} */
|
|
@@ -400,6 +438,22 @@ function isLabelable(element) {
|
|
|
400
438
|
return ['BUTTON', 'INPUT', 'METER', 'OUTPUT', 'PROGRESS', 'SELECT', 'TEXTAREA'].includes(element.tagName);
|
|
401
439
|
}
|
|
402
440
|
|
|
441
|
+
/**
|
|
442
|
+
* The first form control inside `root` that nothing names yet — no `aria-label`, no
|
|
443
|
+
* `aria-labelledby`, no `<label>` with any text — or null.
|
|
444
|
+
* @param {Element} root
|
|
445
|
+
* @returns {Element|null}
|
|
446
|
+
*/
|
|
447
|
+
function unnamedControlWithin(root) {
|
|
448
|
+
for (const candidate of root.querySelectorAll('input:not([type="hidden"]), select, textarea')) {
|
|
449
|
+
if (candidate.hasAttribute('aria-label') || candidate.hasAttribute('aria-labelledby')) continue;
|
|
450
|
+
const labels = [.../** @type {HTMLInputElement} */ (candidate).labels ?? []];
|
|
451
|
+
if (labels.some((label) => label.textContent?.trim())) continue;
|
|
452
|
+
return candidate;
|
|
453
|
+
}
|
|
454
|
+
return null;
|
|
455
|
+
}
|
|
456
|
+
|
|
403
457
|
/** @param {HTMLElement} element @param {string[]} names @returns {Map<string, string|null>} */
|
|
404
458
|
function rememberAttributes(element, names) {
|
|
405
459
|
return new Map(names.map((name) => [name, element.getAttribute(name)]));
|
|
@@ -413,25 +467,32 @@ function restoreAttributes(element, attributes) {
|
|
|
413
467
|
}
|
|
414
468
|
}
|
|
415
469
|
|
|
416
|
-
/**
|
|
417
|
-
|
|
470
|
+
/**
|
|
471
|
+
* The properties a native control is built with: the adapter's own `base` over the caller's
|
|
472
|
+
* `props`, with the field name as the control's `name` unless `props` gives one.
|
|
473
|
+
* @param {Field} field Field the control belongs to.
|
|
474
|
+
* @param {Readonly<FieldOptions>} options Field options.
|
|
475
|
+
* @param {Record<string, unknown>} base Properties the adapter owns.
|
|
476
|
+
* @returns {Record<string, unknown>}
|
|
477
|
+
*/
|
|
478
|
+
function controlProps(field, options, base) {
|
|
418
479
|
const props = { ...(options.props ?? {}) };
|
|
419
480
|
for (const key of Object.keys(props)) {
|
|
420
481
|
if (/^on[a-z]/i.test(key) || key === 'content') delete props[key];
|
|
421
482
|
}
|
|
422
|
-
return { ...props, ...base };
|
|
483
|
+
return { name: field.name || undefined, ...props, ...base };
|
|
423
484
|
}
|
|
424
485
|
|
|
425
486
|
/** @param {'input'|'textarea'} tag @param {string} type @returns {FieldAdapterFactory} */
|
|
426
487
|
function textAdapter(tag, type) {
|
|
427
|
-
return (
|
|
428
|
-
const el = h(tag, controlProps(options, {
|
|
488
|
+
return (field, options) => {
|
|
489
|
+
const el = h(tag, controlProps(field, options, {
|
|
429
490
|
type: tag === 'input' ? type : undefined,
|
|
430
491
|
placeholder: options.placeholder,
|
|
431
492
|
required: options.required,
|
|
432
493
|
disabled: options.disabled
|
|
433
494
|
}));
|
|
434
|
-
return elementValueAdapter(el);
|
|
495
|
+
return { ...elementValueAdapter(el), setReadonly: (readonly) => { el.readOnly = readonly; } };
|
|
435
496
|
};
|
|
436
497
|
}
|
|
437
498
|
|
|
@@ -450,7 +511,7 @@ function elementValueAdapter(el) {
|
|
|
450
511
|
function numericAdapter(kind) {
|
|
451
512
|
return (field, options) => {
|
|
452
513
|
const coerce = kind === 'int' ? coerceInteger : coerceFloat;
|
|
453
|
-
const input = h('input', controlProps(options, {
|
|
514
|
+
const input = h('input', controlProps(field, options, {
|
|
454
515
|
type: 'text',
|
|
455
516
|
inputMode: kind === 'int' ? 'numeric' : 'decimal',
|
|
456
517
|
placeholder: options.placeholder,
|
|
@@ -468,14 +529,15 @@ function numericAdapter(kind) {
|
|
|
468
529
|
});
|
|
469
530
|
return {
|
|
470
531
|
...elementValueAdapter(input),
|
|
471
|
-
get: () => coerce(input.value).value
|
|
532
|
+
get: () => coerce(input.value).value,
|
|
533
|
+
setReadonly: (readonly) => { input.readOnly = readonly; }
|
|
472
534
|
};
|
|
473
535
|
};
|
|
474
536
|
}
|
|
475
537
|
|
|
476
538
|
/** @type {FieldAdapterFactory} */
|
|
477
|
-
const checkboxAdapter = (
|
|
478
|
-
const input = h('input', controlProps(options, {
|
|
539
|
+
const checkboxAdapter = (field, options) => {
|
|
540
|
+
const input = h('input', controlProps(field, options, {
|
|
479
541
|
type: 'checkbox',
|
|
480
542
|
required: options.required,
|
|
481
543
|
disabled: options.disabled
|
|
@@ -490,8 +552,8 @@ const checkboxAdapter = (_field, options) => {
|
|
|
490
552
|
};
|
|
491
553
|
|
|
492
554
|
/** @type {FieldAdapterFactory} */
|
|
493
|
-
const selectAdapter = (
|
|
494
|
-
const select = h('select', controlProps(options, {
|
|
555
|
+
const selectAdapter = (field, options) => {
|
|
556
|
+
const select = h('select', controlProps(field, options, {
|
|
495
557
|
required: options.required,
|
|
496
558
|
disabled: options.disabled
|
|
497
559
|
}));
|
|
@@ -502,12 +564,15 @@ const selectAdapter = (_field, options) => {
|
|
|
502
564
|
};
|
|
503
565
|
|
|
504
566
|
/** @type {FieldAdapterFactory} */
|
|
505
|
-
const optionListAdapter = (
|
|
567
|
+
const optionListAdapter = (field, options) => {
|
|
506
568
|
const root = h('div', {
|
|
507
569
|
class: 'zx-field__option-list',
|
|
508
570
|
role: 'radiogroup'
|
|
509
571
|
});
|
|
510
|
-
|
|
572
|
+
// The radios are grouped by the control's id, not by the field key: radios outside a <form>
|
|
573
|
+
// share one group per name across the whole document, so the same fieldset mounted twice
|
|
574
|
+
// would otherwise check one copy's option by clicking the other's.
|
|
575
|
+
const name = field.controlId;
|
|
511
576
|
const radios = normalizeOptions(options.options).map(([value, label], index) => {
|
|
512
577
|
const input = h('input', {
|
|
513
578
|
type: 'radio',
|
|
@@ -534,8 +599,8 @@ const optionListAdapter = (_field, options) => {
|
|
|
534
599
|
};
|
|
535
600
|
|
|
536
601
|
/** @type {FieldAdapterFactory} */
|
|
537
|
-
const htmlAdapter = (
|
|
538
|
-
const el = h('div', controlProps(options, { class: 'zx-field__static' }));
|
|
602
|
+
const htmlAdapter = (field, options) => {
|
|
603
|
+
const el = h('div', controlProps(field, options, { class: 'zx-field__static', name: undefined }));
|
|
539
604
|
let adopted = null;
|
|
540
605
|
const set = (value) => {
|
|
541
606
|
restoreAdopted(adopted);
|
|
@@ -600,6 +665,13 @@ Field.register('custom', customAdapter);
|
|
|
600
665
|
* @type {CustomEvent<{value: unknown}>}
|
|
601
666
|
*/
|
|
602
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Live edit before it is committed, from the types whose widget tells the two apart (`code`,
|
|
670
|
+
* `slider`): every keystroke or drag step, where `change` follows only on commit.
|
|
671
|
+
* @event Field#input
|
|
672
|
+
* @type {CustomEvent<{value: unknown}>}
|
|
673
|
+
*/
|
|
674
|
+
|
|
603
675
|
/**
|
|
604
676
|
* Field validation failure event.
|
|
605
677
|
* @event Field#invalid
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { isElement } from '../../core/util.js';
|
|
2
|
+
|
|
3
|
+
/** @typedef {import('../field/field.js').Field} Field */
|
|
4
|
+
/** @typedef {import('../field/field.js').FieldAdapter} FieldAdapter */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* An element or component a Fieldset adopts with `addField(id, source)`: anything that renders
|
|
8
|
+
* into an element. Components are read through the methods they have, elements through their
|
|
9
|
+
* native properties.
|
|
10
|
+
* @typedef {Element|{el?: Element, toElement?: () => Element}} ExternalSource
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The value accessor pairs the library's components use, in the order they are tried. A reader
|
|
15
|
+
* counts only when it takes no required argument (`Fieldset.getValue(id)` is not a reader), a
|
|
16
|
+
* writer of another pair only when the value is its sole required argument.
|
|
17
|
+
* - `getValue/setValue`: Field, CodeEditor, Filter, SortControl, Toggle (`getValue()` is the
|
|
18
|
+
* configured on-value or `false`, the same as the `toggle` field type reads).
|
|
19
|
+
* - `get/set`: NumberField, Select, Slider, Rating, CheckButton, Datebox, Timebox, Search,
|
|
20
|
+
* CopyInput, DatePicker and the other pickers.
|
|
21
|
+
* - `getValues/setValues`: Checklist, TagPicker, ValueList, MultiValueEditor, Fieldset, Form.
|
|
22
|
+
* - `isChecked/setChecked`: boolean controls that name their state that way.
|
|
23
|
+
* @type {ReadonlyArray<readonly [string, string]>}
|
|
24
|
+
*/
|
|
25
|
+
export const VALUE_METHODS = Object.freeze([
|
|
26
|
+
Object.freeze(['getValue', 'setValue']),
|
|
27
|
+
Object.freeze(['get', 'set']),
|
|
28
|
+
Object.freeze(['getValues', 'setValues']),
|
|
29
|
+
Object.freeze(['isChecked', 'setChecked'])
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** Value properties a component may expose instead of methods, in the order they are tried. */
|
|
33
|
+
const VALUE_PROPERTIES = Object.freeze(['value', 'values', 'checked']);
|
|
34
|
+
|
|
35
|
+
/** Form controls the fallback disables or makes read-only inside a plain wrapper element. */
|
|
36
|
+
const CONTROLS = 'input, select, textarea, button';
|
|
37
|
+
const TEXT_CONTROLS = 'input, textarea';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The element a source renders into.
|
|
41
|
+
* @param {ExternalSource} source Element or component.
|
|
42
|
+
* @returns {Element}
|
|
43
|
+
*/
|
|
44
|
+
export function externalElement(source) {
|
|
45
|
+
if (isElement(source)) return /** @type {Element} */ (source);
|
|
46
|
+
const component = /** @type {{el?: Element, toElement?: () => Element}} */ (source);
|
|
47
|
+
if (typeof component.toElement === 'function') return component.toElement();
|
|
48
|
+
if (isElement(component.el)) return /** @type {Element} */ (component.el);
|
|
49
|
+
throw new TypeError('External field source must be an Element or a Component');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Reads the current value of an element or component through the first value dialect it
|
|
54
|
+
* supports (see {@link VALUE_METHODS}), then a `value`, `values`, or `checked` property, then the
|
|
55
|
+
* element itself: `checked` for a checkbox or radio, `value` for any other control. Null when
|
|
56
|
+
* the source has no value at all.
|
|
57
|
+
* @param {ExternalSource} source Element or component.
|
|
58
|
+
* @returns {unknown}
|
|
59
|
+
*/
|
|
60
|
+
export function readExternalValue(source) {
|
|
61
|
+
return valueAccess(source, externalElement(source)).get();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Reports whether an element or component is disabled: its `isDisabled()`, a `disabled`
|
|
66
|
+
* property, the element's own `disabled`, an `aria-disabled="true"` or `data-disabled` root, or
|
|
67
|
+
* a wrapper whose every form control is disabled.
|
|
68
|
+
* @param {ExternalSource} source Element or component.
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
export function readExternalDisabled(source) {
|
|
72
|
+
const el = externalElement(source);
|
|
73
|
+
const component = isElement(source) ? null : /** @type {Record<string, any>} */ (source);
|
|
74
|
+
if (component && callable(component, 'isDisabled', 0)) return Boolean(component.isDisabled());
|
|
75
|
+
if (component && typeof component.disabled === 'boolean') return component.disabled;
|
|
76
|
+
if (hasNativeProperty(el, 'disabled')) return Boolean(/** @type {any} */ (el).disabled) || el.hasAttribute('disabled');
|
|
77
|
+
if (el.getAttribute('aria-disabled') === 'true') return true;
|
|
78
|
+
const flag = el.getAttribute('data-disabled');
|
|
79
|
+
if (flag !== null && flag !== 'false') return true;
|
|
80
|
+
const controls = [...el.querySelectorAll(CONTROLS)];
|
|
81
|
+
return controls.length > 0 && controls.every((control) => /** @type {any} */ (control).disabled);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Reports whether an element or component is read-only: a `readonly`/`readOnly` property, the
|
|
86
|
+
* element's own `readOnly`, or an `aria-readonly="true"` or `data-readonly` root.
|
|
87
|
+
* @param {ExternalSource} source Element or component.
|
|
88
|
+
* @returns {boolean}
|
|
89
|
+
*/
|
|
90
|
+
export function readExternalReadonly(source) {
|
|
91
|
+
const el = externalElement(source);
|
|
92
|
+
const component = isElement(source) ? null : /** @type {Record<string, any>} */ (source);
|
|
93
|
+
for (const name of ['readonly', 'readOnly']) {
|
|
94
|
+
if (component && typeof component[name] === 'boolean') return component[name];
|
|
95
|
+
}
|
|
96
|
+
if (hasNativeProperty(el, 'readOnly')) return Boolean(/** @type {any} */ (el).readOnly) || el.hasAttribute('readonly');
|
|
97
|
+
if (el.getAttribute('aria-readonly') === 'true') return true;
|
|
98
|
+
const flag = el.getAttribute('data-readonly');
|
|
99
|
+
return flag !== null && flag !== 'false';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds the Field adapter for an element or component that is not a registered field type.
|
|
104
|
+
*
|
|
105
|
+
* Values go through the source's own accessors ({@link readExternalValue}); disabling through
|
|
106
|
+
* `setDisabled(bool)`, then `enable()`/`disable()`, then a `disabled` property, and only then the
|
|
107
|
+
* DOM — the element's `disabled`, or for a plain wrapper `aria-disabled` plus `disabled` on every
|
|
108
|
+
* form control inside it. Read-only goes through `setReadonly(bool)`/`setReadOnly(bool)`, a
|
|
109
|
+
* `readonly`/`readOnly` property, or the element's own `readOnly`.
|
|
110
|
+
*
|
|
111
|
+
* Writes pass the caller's options to the component: `Field.setValue(v)` or
|
|
112
|
+
* `Fieldset.setValues(values)` without `silent` lets the component emit its own `change` to its
|
|
113
|
+
* own listeners, while the field emits one `change` for the write; with `silent` neither does.
|
|
114
|
+
*
|
|
115
|
+
* Given the field, a component's own `change` event becomes the field's `change`, and the native
|
|
116
|
+
* `change` its inner controls bubble is dropped unless it carries a value the component did not
|
|
117
|
+
* already report — so one edit is one field `change`, whether the component reports it, its
|
|
118
|
+
* input does, or (Search, CopyInput) only its input does.
|
|
119
|
+
*
|
|
120
|
+
* `destroy()` puts the element back where it was found and undoes what the adapter changed:
|
|
121
|
+
* attributes, the native value, disabled and read-only state.
|
|
122
|
+
* @param {ExternalSource} source Element or component to adopt.
|
|
123
|
+
* @param {Field|null} [field=null] The field the adapter serves; needed to forward events. Its
|
|
124
|
+
* `listen()` owns every listener the adapter adds, so destroying the field removes them.
|
|
125
|
+
* @returns {FieldAdapter}
|
|
126
|
+
*/
|
|
127
|
+
export function externalAdapter(source, field = null) {
|
|
128
|
+
const el = externalElement(source);
|
|
129
|
+
const component = isElement(source) ? null : /** @type {Record<string, any>} */ (source);
|
|
130
|
+
const parent = el.parentNode;
|
|
131
|
+
const next = el.nextSibling;
|
|
132
|
+
const attributes = new Map(['id', 'aria-labelledby', 'aria-describedby', 'aria-disabled', 'aria-invalid']
|
|
133
|
+
.map((name) => [name, el.getAttribute(name)]));
|
|
134
|
+
const native = component === null;
|
|
135
|
+
const originalValue = native && hasNativeProperty(el, 'value') ? /** @type {any} */ (el).value : undefined;
|
|
136
|
+
const originalChecked = native && isCheckable(el) ? /** @type {any} */ (el).checked : undefined;
|
|
137
|
+
const originalDisabled = readExternalDisabled(source);
|
|
138
|
+
const originalReadonly = readExternalReadonly(source);
|
|
139
|
+
/** @type {Map<Element, {disabled?: boolean, readOnly?: boolean}>} */
|
|
140
|
+
const poked = new Map();
|
|
141
|
+
const value = valueAccess(source, el);
|
|
142
|
+
const disabled = disabledAccess(component, el, poked);
|
|
143
|
+
const readonly = readonlyAccess(component, el, poked);
|
|
144
|
+
let lastDisabled = originalDisabled;
|
|
145
|
+
let lastReadonly = originalReadonly;
|
|
146
|
+
let setting = false;
|
|
147
|
+
let reported = value.get();
|
|
148
|
+
|
|
149
|
+
if (field && component && typeof component.addEventListener === 'function') {
|
|
150
|
+
// Through the field's `listen()`, so destroying the field unsubscribes. A write through
|
|
151
|
+
// `set()` is not forwarded: the Field reports that one itself.
|
|
152
|
+
field.listen(/** @type {any} */ (component), 'change', () => {
|
|
153
|
+
if (setting) return;
|
|
154
|
+
reported = value.get();
|
|
155
|
+
field.emit('change', { value: reported });
|
|
156
|
+
});
|
|
157
|
+
// Registered before the Field's own `change` listener on the same element, so a native
|
|
158
|
+
// change the component already reported never reaches it.
|
|
159
|
+
field.listen(el, 'change', (event) => {
|
|
160
|
+
const current = value.get();
|
|
161
|
+
if (sameValue(current, reported)) event.stopImmediatePropagation();
|
|
162
|
+
else reported = current;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** @type {FieldAdapter} */
|
|
167
|
+
const adapter = {
|
|
168
|
+
el,
|
|
169
|
+
get: () => value.get(),
|
|
170
|
+
set: (next, options) => {
|
|
171
|
+
setting = true;
|
|
172
|
+
try { value.set(next, options); }
|
|
173
|
+
finally { setting = false; }
|
|
174
|
+
reported = value.get();
|
|
175
|
+
},
|
|
176
|
+
focus: () => {
|
|
177
|
+
if (component && callable(component, 'focus', 0)) component.focus();
|
|
178
|
+
else if (typeof (/** @type {any} */ (el).focus) === 'function') /** @type {any} */ (el).focus();
|
|
179
|
+
},
|
|
180
|
+
setDisabled: (state) => {
|
|
181
|
+
lastDisabled = Boolean(state);
|
|
182
|
+
disabled(lastDisabled);
|
|
183
|
+
},
|
|
184
|
+
destroy: () => {
|
|
185
|
+
if (lastReadonly !== originalReadonly && readonly) readonly(originalReadonly);
|
|
186
|
+
if (lastDisabled !== originalDisabled) disabled(originalDisabled);
|
|
187
|
+
for (const [control, state] of poked) {
|
|
188
|
+
if (state.disabled !== undefined) /** @type {any} */ (control).disabled = state.disabled;
|
|
189
|
+
if (state.readOnly !== undefined) /** @type {any} */ (control).readOnly = state.readOnly;
|
|
190
|
+
}
|
|
191
|
+
poked.clear();
|
|
192
|
+
for (const [name, saved] of attributes) {
|
|
193
|
+
if (saved === null) el.removeAttribute(name);
|
|
194
|
+
else el.setAttribute(name, saved);
|
|
195
|
+
}
|
|
196
|
+
if (originalValue !== undefined) /** @type {any} */ (el).value = originalValue;
|
|
197
|
+
if (originalChecked !== undefined) /** @type {any} */ (el).checked = originalChecked;
|
|
198
|
+
if (parent) parent.insertBefore(el, next?.parentNode === parent ? next : null);
|
|
199
|
+
else el.remove();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
if (readonly) {
|
|
203
|
+
adapter.setReadonly = (state) => {
|
|
204
|
+
lastReadonly = Boolean(state);
|
|
205
|
+
readonly(lastReadonly);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return adapter;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* The value reader and writer for a source: the first {@link VALUE_METHODS} pair whose reader
|
|
213
|
+
* exists (its own writer first, then any other dialect's), then a value property, then the
|
|
214
|
+
* element.
|
|
215
|
+
* @param {ExternalSource} source Element or component.
|
|
216
|
+
* @param {Element} el Its element.
|
|
217
|
+
* @returns {{get: () => unknown, set: (value: unknown, options?: {silent?: boolean}) => void}}
|
|
218
|
+
*/
|
|
219
|
+
function valueAccess(source, el) {
|
|
220
|
+
const component = isElement(source) ? null : /** @type {Record<string, any>} */ (source);
|
|
221
|
+
if (component) {
|
|
222
|
+
const pair = VALUE_METHODS.find(([reader]) => callable(component, reader, 0));
|
|
223
|
+
// The pair's own writer whatever its signature; another dialect's only when the value is its
|
|
224
|
+
// sole required argument.
|
|
225
|
+
const writer = !pair ? undefined : callable(component, pair[1], 2)
|
|
226
|
+
? pair[1]
|
|
227
|
+
: VALUE_METHODS.map(([, name]) => name).find((name) => callable(component, name, 1));
|
|
228
|
+
if (pair) {
|
|
229
|
+
return {
|
|
230
|
+
get: () => component[pair[0]](),
|
|
231
|
+
// The caller's options pass through: a write that is not silent reaches the component's
|
|
232
|
+
// own `change` listeners too, and the adapter does not forward it a second time.
|
|
233
|
+
set: (next, options = {}) => {
|
|
234
|
+
if (writer) component[writer](next, { ...options, silent: Boolean(options.silent) });
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const property = VALUE_PROPERTIES.find((name) => name in component);
|
|
239
|
+
if (property) {
|
|
240
|
+
return {
|
|
241
|
+
get: () => component[property],
|
|
242
|
+
set: (next) => { component[property] = next; }
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (isCheckable(el)) {
|
|
247
|
+
return {
|
|
248
|
+
get: () => Boolean(/** @type {any} */ (el).checked),
|
|
249
|
+
set: (next) => { /** @type {any} */ (el).checked = Boolean(next); }
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (hasNativeProperty(el, 'value')) {
|
|
253
|
+
return {
|
|
254
|
+
get: () => /** @type {any} */ (el).value,
|
|
255
|
+
set: (next) => { /** @type {any} */ (el).value = next == null ? '' : next; }
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
return { get: () => null, set: () => {} };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The disabling dialect for a source.
|
|
263
|
+
* @param {Record<string, any>|null} component Component, or null for a plain element.
|
|
264
|
+
* @param {Element} el Its element.
|
|
265
|
+
* @param {Map<Element, {disabled?: boolean, readOnly?: boolean}>} poked Original states of the
|
|
266
|
+
* controls the DOM fallback touched.
|
|
267
|
+
* @returns {(disabled: boolean) => void}
|
|
268
|
+
*/
|
|
269
|
+
function disabledAccess(component, el, poked) {
|
|
270
|
+
if (component && callable(component, 'setDisabled', 1)) return (state) => component.setDisabled(state);
|
|
271
|
+
if (component && callable(component, 'enable', 0) && callable(component, 'disable', 0)) {
|
|
272
|
+
return (state) => (state ? component.disable() : component.enable());
|
|
273
|
+
}
|
|
274
|
+
if (component && typeof component.disabled === 'boolean') return (state) => { component.disabled = state; };
|
|
275
|
+
if (hasNativeProperty(el, 'disabled')) return (state) => { /** @type {any} */ (el).disabled = state; };
|
|
276
|
+
return (state) => {
|
|
277
|
+
el.setAttribute('aria-disabled', String(state));
|
|
278
|
+
for (const control of el.querySelectorAll(CONTROLS)) {
|
|
279
|
+
remember(poked, control, 'disabled');
|
|
280
|
+
/** @type {any} */ (control).disabled = state || Boolean(poked.get(control)?.disabled);
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The read-only dialect for a source, or null when it has none.
|
|
287
|
+
* @param {Record<string, any>|null} component Component, or null for a plain element.
|
|
288
|
+
* @param {Element} el Its element.
|
|
289
|
+
* @param {Map<Element, {disabled?: boolean, readOnly?: boolean}>} poked Original states of the
|
|
290
|
+
* controls the DOM fallback touched.
|
|
291
|
+
* @returns {((readonly: boolean) => void)|null}
|
|
292
|
+
*/
|
|
293
|
+
function readonlyAccess(component, el, poked) {
|
|
294
|
+
if (component) {
|
|
295
|
+
for (const name of ['setReadonly', 'setReadOnly']) {
|
|
296
|
+
if (callable(component, name, 1)) return (state) => component[name](state);
|
|
297
|
+
}
|
|
298
|
+
for (const name of ['readonly', 'readOnly']) {
|
|
299
|
+
if (typeof component[name] === 'boolean') return (state) => { component[name] = state; };
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
if (hasNativeProperty(el, 'readOnly')) return (state) => { /** @type {any} */ (el).readOnly = state; };
|
|
304
|
+
const controls = [...el.querySelectorAll(TEXT_CONTROLS)];
|
|
305
|
+
if (!controls.length) return null;
|
|
306
|
+
return (state) => {
|
|
307
|
+
for (const control of controls) {
|
|
308
|
+
remember(poked, control, 'readOnly');
|
|
309
|
+
/** @type {any} */ (control).readOnly = state || Boolean(poked.get(control)?.readOnly);
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* @param {Map<Element, {disabled?: boolean, readOnly?: boolean}>} poked
|
|
316
|
+
* @param {Element} control
|
|
317
|
+
* @param {'disabled'|'readOnly'} name
|
|
318
|
+
* @returns {void}
|
|
319
|
+
*/
|
|
320
|
+
function remember(poked, control, name) {
|
|
321
|
+
const state = poked.get(control) ?? {};
|
|
322
|
+
if (state[name] === undefined) state[name] = Boolean(/** @type {any} */ (control)[name]);
|
|
323
|
+
poked.set(control, state);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Whether `target[name]` is a method taking at most `arity` required arguments.
|
|
328
|
+
* @param {Record<string, any>} target
|
|
329
|
+
* @param {string} name
|
|
330
|
+
* @param {number} arity
|
|
331
|
+
* @returns {boolean}
|
|
332
|
+
*/
|
|
333
|
+
function callable(target, name, arity) {
|
|
334
|
+
const fn = target[name];
|
|
335
|
+
return typeof fn === 'function' && fn.length <= arity;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** @param {Element} el @param {string} name @returns {boolean} */
|
|
339
|
+
function hasNativeProperty(el, name) {
|
|
340
|
+
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON', 'FIELDSET', 'OUTPUT'].includes(el.tagName) && name in el;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** @param {Element} el @returns {boolean} */
|
|
344
|
+
function isCheckable(el) {
|
|
345
|
+
const type = String(/** @type {any} */ (el).type ?? '').toLowerCase();
|
|
346
|
+
return el.tagName === 'INPUT' && (type === 'checkbox' || type === 'radio');
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Value equality for change de-duplication: identical primitives, Dates at the same instant,
|
|
351
|
+
* arrays and plain objects with the same JSON.
|
|
352
|
+
* @param {unknown} a
|
|
353
|
+
* @param {unknown} b
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
function sameValue(a, b) {
|
|
357
|
+
if (Object.is(a, b)) return true;
|
|
358
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime();
|
|
359
|
+
if (a && b && typeof a === 'object' && typeof b === 'object') {
|
|
360
|
+
try { return JSON.stringify(a) === JSON.stringify(b); } catch { return false; }
|
|
361
|
+
}
|
|
362
|
+
return false;
|
|
363
|
+
}
|