@vanilla-bean/components 1.1.0 → 2.0.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/Component/Component.js +226 -40
- package/Component/Component.scenarios.js +2 -2
- package/Component/Component.test.js +427 -40
- package/Component/README.md +480 -0
- package/Component/observeElementConnection.js +1 -1
- package/Elem/README.md +373 -0
- package/README.md +2 -2
- package/components/BottomSheet/BottomSheet.js +8 -16
- package/components/BottomSheet/README.md +1 -1
- package/components/Button/Button.js +12 -18
- package/components/Button/Button.lld.md +1 -1
- package/components/Calendar/Calendar.js +139 -59
- package/components/Calendar/CalendarEvent.js +9 -4
- package/components/Calendar/Toolbar.js +28 -20
- package/components/Calendar/index.js +1 -0
- package/components/Code/Code.js +26 -30
- package/components/Code/Code.lld.md +1 -1
- package/components/ColorPicker/ColorPicker.js +58 -58
- package/components/Dialog/Dialog.js +79 -73
- package/components/Dialog/Dialog.lld.md +1 -1
- package/components/Dialog/README.md +10 -10
- package/components/Form/Form.js +21 -17
- package/components/Form/Form.lld.md +2 -2
- package/components/Form/README.md +3 -3
- package/components/Icon/Icon.js +19 -11
- package/components/Input/Input.js +85 -67
- package/components/Input/README.md +7 -9
- package/components/Keyboard/Key.js +7 -10
- package/components/Keyboard/Keyboard.js +38 -46
- package/components/Keyboard/Keyboard.lld.md +1 -1
- package/components/Label/Label.js +52 -50
- package/components/Link/Link.js +14 -20
- package/components/List/List.js +28 -30
- package/components/Menu/Menu.js +19 -10
- package/components/Menu/Menu.lld.md +2 -2
- package/components/Notify/Notify.js +29 -19
- package/components/Notify/Notify.lld.md +2 -2
- package/components/Page/Page.lld.md +1 -1
- package/components/Popover/Popover.js +49 -44
- package/components/RadioButton/RadioButton.js +33 -29
- package/components/RadioButton/RadioButton.lld.md +1 -1
- package/components/Router/README.md +12 -12
- package/components/Router/Router.js +19 -21
- package/components/Select/README.md +5 -5
- package/components/Select/Select.js +25 -31
- package/components/Table/README.md +4 -4
- package/components/Table/Table.js +55 -36
- package/components/Table/Table.lld.md +1 -1
- package/components/TagList/Tag.js +16 -14
- package/components/TagList/TagList.js +15 -8
- package/components/TagList/TagList.lld.md +1 -1
- package/components/Tooltip/Tooltip.js +12 -25
- package/components/TooltipWrapper/TooltipWrapper.js +55 -49
- package/components/TooltipWrapper/TooltipWrapper.lld.md +2 -2
- package/components/Whiteboard/Whiteboard.js +45 -43
- package/components/Whiteboard/Whiteboard.lld.md +1 -1
- package/devTools/build.js +43 -0
- package/devTools/buildTypes.js +322 -0
- package/devTools/createComponent.js +155 -0
- package/devTools/extractJSDoc.js +395 -0
- package/devTools/processTemplate.js +500 -0
- package/devTools/updateComponentIndex.js +16 -0
- package/devTools/updateDemoViewIndex.js +90 -0
- package/eslint.config.cjs +5 -2
- package/index.d.ts +120 -62
- package/package.json +23 -22
- package/spellcheck.config.cjs +3 -0
- package/styled/README.md +329 -0
- package/theme/.test.js +2 -2
- package/theme/README.md +8 -8
- package/theme/colors.js +16 -13
- package/theme/colors.test.js +28 -0
- package/utils/browser.js +1 -1
- package/utils/element.js +2 -2
- package/FontWithASyntaxHighlighter-Regular.woff2 +0 -0
|
@@ -11,19 +11,7 @@ const DialogButton = styled(
|
|
|
11
11
|
`,
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
-
const
|
|
15
|
-
const variant_enum = Object.freeze(['info', 'success', 'warning', 'error']);
|
|
16
|
-
|
|
17
|
-
const defaultOptions = {
|
|
18
|
-
tag: 'dialog',
|
|
19
|
-
size: 'small',
|
|
20
|
-
openOnRender: 16,
|
|
21
|
-
modal: true,
|
|
22
|
-
get appendTo() {
|
|
23
|
-
return document.body;
|
|
24
|
-
},
|
|
25
|
-
registeredEvents: new Set(['close']),
|
|
26
|
-
};
|
|
14
|
+
const defaultOpenDelay = 16;
|
|
27
15
|
|
|
28
16
|
/**
|
|
29
17
|
* Modal and non-modal dialog component with customizable appearance and behavior.
|
|
@@ -149,57 +137,70 @@ class Dialog extends styled(
|
|
|
149
137
|
}
|
|
150
138
|
`,
|
|
151
139
|
) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const openDelay = value ? setTimeout(() => this.open(), delay) : null;
|
|
159
|
-
this.replaceCleanup('openOnRender', () => openDelay && clearTimeout(openDelay));
|
|
140
|
+
static schema = {
|
|
141
|
+
tag: { default: 'dialog' },
|
|
142
|
+
appendTo: {
|
|
143
|
+
get default() {
|
|
144
|
+
return document.body;
|
|
145
|
+
},
|
|
160
146
|
},
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
);
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.removeClass(/\bsize-\S+\b/g);
|
|
172
|
-
|
|
173
|
-
if (value) this.addClass(`size-${value}`);
|
|
147
|
+
openOnRender: {
|
|
148
|
+
default: defaultOpenDelay,
|
|
149
|
+
set(value) {
|
|
150
|
+
const delay = typeof value === 'number' ? value : defaultOpenDelay;
|
|
151
|
+
const openDelay = value ? setTimeout(() => this.open(), delay) : null;
|
|
152
|
+
this.replaceCleanup('openOnRender', () => openDelay && clearTimeout(openDelay));
|
|
153
|
+
},
|
|
174
154
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
155
|
+
// Read directly from this.options.modal in open()
|
|
156
|
+
modal: { default: true },
|
|
157
|
+
size: {
|
|
158
|
+
default: 'small',
|
|
159
|
+
enum: ['small', 'standard', 'large'],
|
|
160
|
+
set(value) {
|
|
161
|
+
this.removeClass(/\bsize-\S+\b/g);
|
|
162
|
+
|
|
163
|
+
if (value) this.addClass(`size-${value}`);
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
variant: {
|
|
167
|
+
enum: ['info', 'success', 'warning', 'error'],
|
|
168
|
+
set(value) {
|
|
169
|
+
this.removeClass(/\bvariant-\S+\b/g);
|
|
183
170
|
|
|
184
|
-
|
|
171
|
+
if (value) this.addClass(`variant-${value}`);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
body: {
|
|
175
|
+
set(value) {
|
|
176
|
+
this.body?.content(value);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
header: {
|
|
180
|
+
set(value) {
|
|
181
|
+
this._header?.content(value);
|
|
182
|
+
},
|
|
185
183
|
},
|
|
186
|
-
|
|
187
|
-
|
|
184
|
+
buttons: {
|
|
185
|
+
set() {
|
|
186
|
+
this._refreshFooter();
|
|
187
|
+
},
|
|
188
188
|
},
|
|
189
|
-
|
|
190
|
-
|
|
189
|
+
footer: {
|
|
190
|
+
set() {
|
|
191
|
+
this._refreshFooter();
|
|
192
|
+
},
|
|
191
193
|
},
|
|
194
|
+
// Read from this.options at button-press time
|
|
195
|
+
closeDialog: {},
|
|
196
|
+
onButtonPress: {},
|
|
192
197
|
};
|
|
193
198
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
constructor(options = {}, ...children) {
|
|
197
|
-
super({ ...defaultOptions, ...options }, ...children);
|
|
199
|
+
static events = ['close'];
|
|
198
200
|
|
|
201
|
+
build() {
|
|
199
202
|
this.options['aria-labelledby'] = this.uniqueId;
|
|
200
|
-
}
|
|
201
203
|
|
|
202
|
-
build() {
|
|
203
204
|
this._header = new Elem({
|
|
204
205
|
tag: 'div',
|
|
205
206
|
id: this.uniqueId,
|
|
@@ -210,29 +211,34 @@ class Dialog extends styled(
|
|
|
210
211
|
|
|
211
212
|
this._body = new Elem({ addClass: ['content'], appendTo: this });
|
|
212
213
|
|
|
213
|
-
this._footer = new Elem({
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
214
|
+
this._footer = new Elem({ addClass: ['footer'], append: this._footerContent(), appendTo: this });
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
_refreshFooter() {
|
|
218
|
+
if (this.rendered) this._footer?.content(this._footerContent() || []);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
_footerContent() {
|
|
222
|
+
return (
|
|
223
|
+
this.options.footer ||
|
|
224
|
+
this.options.buttons?.map(
|
|
225
|
+
button =>
|
|
226
|
+
new DialogButton({
|
|
227
|
+
onPointerPress: event =>
|
|
228
|
+
this.options.onButtonPress?.({
|
|
229
|
+
event,
|
|
230
|
+
button,
|
|
231
|
+
dialog: this,
|
|
232
|
+
closeDialog: this.options.closeDialog || (() => this.close()),
|
|
233
|
+
}),
|
|
234
|
+
...(typeof button === 'object' ? button : { textContent: button }),
|
|
235
|
+
}),
|
|
236
|
+
)
|
|
237
|
+
);
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
/**
|
|
235
|
-
* Content container
|
|
241
|
+
* Content container - primary injection point for subclass and consumer content.
|
|
236
242
|
* @returns {Component} The dialog body component
|
|
237
243
|
*/
|
|
238
244
|
get body() {
|
|
@@ -14,7 +14,7 @@ Native `<dialog>` element wrapper. The decision to use the native element rather
|
|
|
14
14
|
- an unrecognized size or variant throws at the point of assignment, not later when layout breaks
|
|
15
15
|
- does an unrecognized size value throw?
|
|
16
16
|
|
|
17
|
-
## Header, body, and footer are stable references
|
|
17
|
+
## Header, body, and footer are stable references - options update them in place
|
|
18
18
|
|
|
19
19
|
- the structural elements are created once in `build()` and persist across option updates; assigning a new `header` value changes the header's content, not the dialog's structure
|
|
20
20
|
- does updating the header option change the header's content without replacing the dialog structure?
|
|
@@ -22,25 +22,25 @@ const dialog = new Dialog({
|
|
|
22
22
|
|
|
23
23
|
| Option | Type | Default | Description |
|
|
24
24
|
| --- | --- | --- | --- |
|
|
25
|
-
| `header` | `string` |
|
|
26
|
-
| `body` | `string\|Component\|Array` |
|
|
27
|
-
| `buttons` | `Array<string\|object>` |
|
|
28
|
-
| `footer` | `Array<Component>` |
|
|
29
|
-
| `onButtonPress` | `Function` |
|
|
25
|
+
| `header` | `string` | - | Text rendered in the `<h2>` header |
|
|
26
|
+
| `body` | `string\|Component\|Array` | - | Content for the scrollable body section. Reactive: updating `options.body` replaces content |
|
|
27
|
+
| `buttons` | `Array<string\|object>` | - | Shorthand footer buttons. Each entry is a string label or `{ textContent, variant?, onPointerPress?, ...buttonOptions }` |
|
|
28
|
+
| `footer` | `Array<Component>` | - | Fully custom footer components. Takes precedence over `buttons` |
|
|
29
|
+
| `onButtonPress` | `Function` | - | Called when any `buttons` entry is pressed. Receives `{ button, closeDialog, event }` |
|
|
30
30
|
| `openOnRender` | `number\|boolean` | `16` | Auto-open delay in ms after render. Set to `false` to disable auto-open |
|
|
31
31
|
| `modal` | `boolean` | `true` | Open as a modal (with backdrop) vs. non-modal |
|
|
32
32
|
| `size` | `string` | `'small'` | Dialog dimensions: `'small'` (420×210px), `'standard'` (840×420px), `'large'` (90vw×90vh) |
|
|
33
|
-
| `variant` | `string` |
|
|
34
|
-
| `closeDialog` | `Function` |
|
|
33
|
+
| `variant` | `string` | - | Color theme: `'info'`, `'success'`, `'warning'`, `'error'` |
|
|
34
|
+
| `closeDialog` | `Function` | - | Override the default close behavior used inside `onButtonPress` |
|
|
35
35
|
| `appendTo` | `Element` | `document.body` | Where to append the dialog in the DOM |
|
|
36
36
|
|
|
37
37
|
### `onButtonPress` callback signature
|
|
38
38
|
|
|
39
39
|
```js
|
|
40
40
|
onButtonPress: ({ button, closeDialog, event }) => { ... }
|
|
41
|
-
// button
|
|
42
|
-
// closeDialog
|
|
43
|
-
// event
|
|
41
|
+
// button - the original entry from the buttons array (string or object)
|
|
42
|
+
// closeDialog - function that closes the dialog (calls dialog.close() by default)
|
|
43
|
+
// event - the pointer event that triggered the press
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
## Methods
|
package/components/Form/Form.js
CHANGED
|
@@ -14,18 +14,18 @@ import { Label } from '../Label';
|
|
|
14
14
|
* **Field definition:**
|
|
15
15
|
* ```js
|
|
16
16
|
* {
|
|
17
|
-
* key: 'name', // required
|
|
18
|
-
* label: 'Full Name', // optional
|
|
19
|
-
* InputComponent: Select, // optional
|
|
20
|
-
* onChange: event => {}, // optional
|
|
21
|
-
* parse: value => value.trim(), // optional
|
|
22
|
-
* condition: data => data.type === 'x', // optional
|
|
23
|
-
* validate: value => value ? null : 'Required', // optional
|
|
17
|
+
* key: 'name', // required - data key
|
|
18
|
+
* label: 'Full Name', // optional - defaults to formatted key
|
|
19
|
+
* InputComponent: Select, // optional - defaults to Input
|
|
20
|
+
* onChange: event => {}, // optional - called after data update
|
|
21
|
+
* parse: value => value.trim(), // optional - transform before storing
|
|
22
|
+
* condition: data => data.type === 'x', // optional - hide field when false
|
|
23
|
+
* validate: value => value ? null : 'Required', // optional - field-level validator
|
|
24
24
|
* // ...any other InputComponent options
|
|
25
25
|
* }
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
|
-
* **Group definition**
|
|
28
|
+
* **Group definition** - renders inputs side-by-side in a layout container:
|
|
29
29
|
* ```js
|
|
30
30
|
* {
|
|
31
31
|
* type: 'group',
|
|
@@ -65,15 +65,19 @@ export default class Form extends Component {
|
|
|
65
65
|
* Rebuild the form when inputs change without recreating the data store.
|
|
66
66
|
* Existing data is preserved; new keys are added with their initial values.
|
|
67
67
|
*/
|
|
68
|
-
static
|
|
69
|
-
inputs
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
68
|
+
static schema = {
|
|
69
|
+
inputs: {
|
|
70
|
+
set(value) {
|
|
71
|
+
if (!this.rendered || !value || !this.options.data) return;
|
|
72
|
+
this.empty();
|
|
73
|
+
this._resetState();
|
|
74
|
+
this._processInputs(value, this);
|
|
75
|
+
this._wireConditions();
|
|
76
|
+
this._setupAnnouncer();
|
|
77
|
+
},
|
|
76
78
|
},
|
|
79
|
+
// Data store managed by build()
|
|
80
|
+
data: {},
|
|
77
81
|
};
|
|
78
82
|
|
|
79
83
|
/** @private */
|
|
@@ -91,7 +95,7 @@ export default class Form extends Component {
|
|
|
91
95
|
if (!this._conditionals.length) return;
|
|
92
96
|
const evaluate = () => this._evaluateConditions();
|
|
93
97
|
this.options.data.addEventListener('set', evaluate);
|
|
94
|
-
// replaceCleanup
|
|
98
|
+
// replaceCleanup - safe to call multiple times (inputs handler re-wires on rebuild)
|
|
95
99
|
this.replaceCleanup('conditions', () => this.options.data.removeEventListener('set', evaluate));
|
|
96
100
|
}
|
|
97
101
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Declarative form from an array of input configurations. The key design decision is that the form owns data binding. Each input's changes update a shared reactive `data` context, and `hasErrors()` validates the whole form in one call.
|
|
6
6
|
|
|
7
|
-
## Label text falls back to the field key
|
|
7
|
+
## Label text falls back to the field key - no separate label required for obvious fields
|
|
8
8
|
|
|
9
9
|
- if an input config omits `label`, the key name becomes the label
|
|
10
10
|
- does an input with no label option display the key name as its label?
|
|
@@ -15,7 +15,7 @@ Declarative form from an array of input configurations. The key design decision
|
|
|
15
15
|
- does hasErrors return false when all inputs are valid?
|
|
16
16
|
- does hasErrors return true when an input's validation fails?
|
|
17
17
|
|
|
18
|
-
## Form data is live
|
|
18
|
+
## Form data is live - changes are visible before any submit action
|
|
19
19
|
|
|
20
20
|
- each input's value feeds the shared `data` context as it changes
|
|
21
21
|
- does typing in an input field update the form's data context before any submit?
|
|
@@ -20,7 +20,7 @@ const form = new Form({
|
|
|
20
20
|
|
|
21
21
|
| Option | Type | Default | Description |
|
|
22
22
|
| -------- | --------------- | ------- | ----------------------------------------------------------------------- |
|
|
23
|
-
| `inputs` | `Array<object>` |
|
|
23
|
+
| `inputs` | `Array<object>` | - | Input configuration array; see shape below |
|
|
24
24
|
| `data` | `object` | `{}` | Initial form data. Replaced with an `Oxject` instance on each `build()` |
|
|
25
25
|
|
|
26
26
|
### `inputs` entry shape
|
|
@@ -32,8 +32,8 @@ const form = new Form({
|
|
|
32
32
|
| `InputComponent` | `Component class` | `Input` | Component class to instantiate. Can be `Select`, `ColorPicker`, or any component that accepts `value` and `onChange` |
|
|
33
33
|
| `onChange` | `Function` | `() => {}` | Called after the data context is updated with the new value |
|
|
34
34
|
| `parse` | `Function(value, input)` | `v => v` | Transforms the raw event value before storing it in `options.data` |
|
|
35
|
-
| `validations` | `Array` |
|
|
36
|
-
| `...inputOptions` | `any` |
|
|
35
|
+
| `validations` | `Array` | - | Passed directly to the `InputComponent` (see Input validation format) |
|
|
36
|
+
| `...inputOptions` | `any` | - | Any additional options forwarded to the `InputComponent` constructor |
|
|
37
37
|
|
|
38
38
|
### `data` Oxject lifecycle
|
|
39
39
|
|
package/components/Icon/Icon.js
CHANGED
|
@@ -14,20 +14,28 @@ import { Component } from '../../Component';
|
|
|
14
14
|
* @returns {Icon} Icon component instance
|
|
15
15
|
*/
|
|
16
16
|
export default class Icon extends Component {
|
|
17
|
-
static
|
|
18
|
-
icon
|
|
19
|
-
|
|
17
|
+
static schema = {
|
|
18
|
+
icon: {
|
|
19
|
+
set() {
|
|
20
|
+
this._refreshIcon();
|
|
21
|
+
},
|
|
20
22
|
},
|
|
21
|
-
animation
|
|
22
|
-
|
|
23
|
+
animation: {
|
|
24
|
+
set() {
|
|
25
|
+
this._refreshIcon();
|
|
26
|
+
},
|
|
23
27
|
},
|
|
24
|
-
content
|
|
25
|
-
|
|
26
|
-
|
|
28
|
+
content: {
|
|
29
|
+
set(value, next) {
|
|
30
|
+
next(value);
|
|
31
|
+
this._refreshIcon();
|
|
32
|
+
},
|
|
27
33
|
},
|
|
28
|
-
textContent
|
|
29
|
-
|
|
30
|
-
|
|
34
|
+
textContent: {
|
|
35
|
+
set(value, next) {
|
|
36
|
+
next(value);
|
|
37
|
+
this._refreshIcon();
|
|
38
|
+
},
|
|
31
39
|
},
|
|
32
40
|
};
|
|
33
41
|
|
|
@@ -26,13 +26,11 @@ const type_enum = Object.freeze([
|
|
|
26
26
|
'url',
|
|
27
27
|
'week',
|
|
28
28
|
]);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
value: '',
|
|
29
|
+
// Only meaningful on input/textarea elements - child classes rendering other tags (e.g. ColorPicker's div) must not receive them
|
|
30
|
+
const textInputDefaults = {
|
|
32
31
|
autocomplete: 'off',
|
|
33
32
|
autocapitalize: 'off',
|
|
34
33
|
autocorrect: 'off',
|
|
35
|
-
height: 'auto',
|
|
36
34
|
placeholder: '',
|
|
37
35
|
};
|
|
38
36
|
|
|
@@ -59,19 +57,91 @@ const dataTypeToInputType = { number: 'number', boolean: 'checkbox', string: 'te
|
|
|
59
57
|
* @returns {Input} Input component instance
|
|
60
58
|
*/
|
|
61
59
|
export default class Input extends Component {
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
static schema = {
|
|
61
|
+
tag: { default: 'input' },
|
|
62
|
+
type: { enum: type_enum },
|
|
63
|
+
height: {
|
|
64
|
+
default: 'auto',
|
|
65
|
+
set(value) {
|
|
66
|
+
if (this.tag !== 'textarea') return;
|
|
67
|
+
|
|
68
|
+
if (value === 'auto') {
|
|
69
|
+
this.__updateAutoHeight = () => {
|
|
70
|
+
this.elem.style.height = this.options.syntaxHighlighting
|
|
71
|
+
? `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.19}em + 10px)`
|
|
72
|
+
: `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.25}em + 16px)`;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
this.__debouncedUpdateAutoHeight =
|
|
76
|
+
this.__debouncedUpdateAutoHeight ||
|
|
77
|
+
(() => {
|
|
78
|
+
clearTimeout(this.__autoHeightTimeout);
|
|
79
|
+
this.__autoHeightTimeout = setTimeout(this.__updateAutoHeight, 16);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
this.__updateAutoHeight();
|
|
83
|
+
|
|
84
|
+
this.elem.addEventListener('input', this.__debouncedUpdateAutoHeight);
|
|
85
|
+
|
|
86
|
+
this.replaceCleanup('autoHeight', () => {
|
|
87
|
+
clearTimeout(this.__autoHeightTimeout);
|
|
88
|
+
this.elem.removeEventListener?.('input', this.__debouncedUpdateAutoHeight);
|
|
89
|
+
});
|
|
90
|
+
} else {
|
|
91
|
+
this.elem.removeEventListener('input', this.__debouncedUpdateAutoHeight);
|
|
92
|
+
clearTimeout(this.__autoHeightTimeout);
|
|
64
93
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
this.elem.style.height = typeof value === 'number' ? `${value + 1}em` : value;
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
value: {
|
|
99
|
+
default: '',
|
|
100
|
+
set(value) {
|
|
101
|
+
if (this.options.type === 'checkbox') this.elem.checked = value;
|
|
102
|
+
else this.elem.value = value;
|
|
103
|
+
|
|
104
|
+
if (this.rendered && this.options.validations?.length) retry(() => this.validate(), { delay: 500, max: 1 });
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
syntaxHighlighting: {
|
|
108
|
+
set(value) {
|
|
109
|
+
this[value ? 'addClass' : 'removeClass']('syntax-highlighting');
|
|
72
110
|
},
|
|
73
|
-
|
|
74
|
-
|
|
111
|
+
},
|
|
112
|
+
language: {
|
|
113
|
+
set(value) {
|
|
114
|
+
if (!this.options.syntaxHighlighting) return;
|
|
115
|
+
this.removeClass(/\blanguage-\S+\b/g);
|
|
116
|
+
this.addClass(`language-${value}`);
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
autocorrect: {
|
|
120
|
+
set(value) {
|
|
121
|
+
this.elem.autocorrect = value;
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
// Read by validate() and the value handler
|
|
125
|
+
validations: {
|
|
126
|
+
get default() {
|
|
127
|
+
return [];
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
static prepareOptions(options) {
|
|
133
|
+
const rawValue = options.value?.__isDerived ? options.value.toJSON() : options.value;
|
|
134
|
+
const tag = options.tag || 'input';
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
...(tag === 'input' ? { type: dataTypeToInputType[typeof rawValue] || 'text' } : {}),
|
|
138
|
+
...(tag === 'input' || tag === 'textarea' ? textInputDefaults : {}),
|
|
139
|
+
...options,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
constructor(options = {}, ...children) {
|
|
144
|
+
super(options, ...children);
|
|
75
145
|
|
|
76
146
|
this.initialValue = this.options.value;
|
|
77
147
|
}
|
|
@@ -93,58 +163,6 @@ export default class Input extends Component {
|
|
|
93
163
|
}
|
|
94
164
|
}
|
|
95
165
|
|
|
96
|
-
static handlers = {
|
|
97
|
-
height(value) {
|
|
98
|
-
if (this.tag !== 'textarea') return;
|
|
99
|
-
|
|
100
|
-
if (value === 'auto') {
|
|
101
|
-
this.__updateAutoHeight = () => {
|
|
102
|
-
this.elem.style.height = this.options.syntaxHighlighting
|
|
103
|
-
? `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.19}em + 10px)`
|
|
104
|
-
: `calc(${((this.elem.value?.match(/\n/g) || '').length + 1) * 1.25}em + 16px)`;
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
this.__debouncedUpdateAutoHeight =
|
|
108
|
-
this.__debouncedUpdateAutoHeight ||
|
|
109
|
-
(() => {
|
|
110
|
-
clearTimeout(this.__autoHeightTimeout);
|
|
111
|
-
this.__autoHeightTimeout = setTimeout(this.__updateAutoHeight, 16);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
this.__updateAutoHeight();
|
|
115
|
-
|
|
116
|
-
this.elem.addEventListener('input', this.__debouncedUpdateAutoHeight);
|
|
117
|
-
|
|
118
|
-
this.replaceCleanup('autoHeight', () => {
|
|
119
|
-
clearTimeout(this.__autoHeightTimeout);
|
|
120
|
-
this.elem.removeEventListener?.('input', this.__debouncedUpdateAutoHeight);
|
|
121
|
-
});
|
|
122
|
-
} else {
|
|
123
|
-
this.elem.removeEventListener('input', this.__debouncedUpdateAutoHeight);
|
|
124
|
-
clearTimeout(this.__autoHeightTimeout);
|
|
125
|
-
|
|
126
|
-
this.elem.style.height = typeof value === 'number' ? `${value + 1}em` : value;
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
value(value) {
|
|
130
|
-
if (this.options.type === 'checkbox') this.elem.checked = value;
|
|
131
|
-
else this.elem.value = value;
|
|
132
|
-
|
|
133
|
-
if (this.rendered && this.options.validations?.length) retry(() => this.validate(), { delay: 500, max: 1 });
|
|
134
|
-
},
|
|
135
|
-
syntaxHighlighting(value) {
|
|
136
|
-
this[value ? 'addClass' : 'removeClass']('syntax-highlighting');
|
|
137
|
-
},
|
|
138
|
-
language(value) {
|
|
139
|
-
if (!this.options.syntaxHighlighting) return;
|
|
140
|
-
this.removeClass(/\blanguage-\S+\b/g);
|
|
141
|
-
this.addClass(`language-${value}`);
|
|
142
|
-
},
|
|
143
|
-
autocorrect(value) {
|
|
144
|
-
this.elem.autocorrect = value;
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
|
|
148
166
|
/**
|
|
149
167
|
* Checks if the input value has changed from its initial value
|
|
150
168
|
* @returns {boolean} True if value is no longer the initial value
|
|
@@ -27,12 +27,12 @@ const input = new Input({
|
|
|
27
27
|
| `autocapitalize` | `string` | `'off'` | Native autocapitalize attribute |
|
|
28
28
|
| `autocorrect` | `string` | `'off'` | Native autocorrect attribute |
|
|
29
29
|
| `height` | `string\|number` | `'auto'` | Height for textarea. `'auto'` dynamically sizes to content; a number sets `em` units |
|
|
30
|
-
| `syntaxHighlighting` | `boolean` |
|
|
31
|
-
| `language` | `string` |
|
|
32
|
-
| `validations` | `Array` |
|
|
33
|
-
| `onInput` | `Function` |
|
|
34
|
-
| `onChange` | `Function` |
|
|
35
|
-
| `onKeyUp` | `Function` |
|
|
30
|
+
| `syntaxHighlighting` | `boolean` | - | Adds `syntaxHighlighting` class and Tab key indentation handling (textarea only) |
|
|
31
|
+
| `language` | `string` | - | Adds `language-<value>` class for Prism-compatible highlighting (requires `syntaxHighlighting`) |
|
|
32
|
+
| `validations` | `Array` | - | Validation rules; see Validation section below |
|
|
33
|
+
| `onInput` | `Function` | - | Called on every keystroke with an enhanced event containing `.value` |
|
|
34
|
+
| `onChange` | `Function` | - | Called on change with an enhanced event containing `.value` |
|
|
35
|
+
| `onKeyUp` | `Function` | - | Called on keyup with an enhanced event containing `.value` |
|
|
36
36
|
|
|
37
37
|
### Supported `type` values
|
|
38
38
|
|
|
@@ -62,15 +62,13 @@ input.validate({ validations?, value? }): Array | undefined
|
|
|
62
62
|
// Runs validation rules. Returns array of error messages if invalid, undefined if valid.
|
|
63
63
|
// Calling with no args uses options.validations and options.value.
|
|
64
64
|
|
|
65
|
-
input.isDirty: boolean // getter
|
|
65
|
+
input.isDirty: boolean // getter - true if current value differs from the initial value
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
## Events
|
|
69
69
|
|
|
70
70
|
Input components emit enhanced native events. All handlers (`onInput`, `onChange`, `onKeyUp`, etc.) receive a standard DOM event augmented with a `.value` property containing the current input value.
|
|
71
71
|
|
|
72
|
-
The `close` event is registered by default via `registeredEvents`.
|
|
73
|
-
|
|
74
72
|
## Example
|
|
75
73
|
|
|
76
74
|
Textarea with auto-height and syntax highlighting:
|
|
@@ -21,17 +21,14 @@ const StyledButton = styled(
|
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
class Key extends StyledButton {
|
|
24
|
-
|
|
25
|
-
const { key, text, class: keyClass, ...
|
|
24
|
+
static prepareOptions(options) {
|
|
25
|
+
const { key, text, class: keyClass, ...rest } = options;
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
},
|
|
33
|
-
...children,
|
|
34
|
-
);
|
|
27
|
+
return {
|
|
28
|
+
textContent: text ?? key,
|
|
29
|
+
...rest,
|
|
30
|
+
addClass: [key, keyClass].concat(options.addClass),
|
|
31
|
+
};
|
|
35
32
|
}
|
|
36
33
|
}
|
|
37
34
|
|