bare-tui-form 0.0.0 → 0.0.1

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/fields/base.js ADDED
@@ -0,0 +1,213 @@
1
+ // Field — the base class every form field extends.
2
+ //
3
+ // A field adapts a bare-tui control (textinput, select, radio, …) to the form:
4
+ // it carries a value `key`, a `label`/`description`, an optional `validate`
5
+ // function, and the current `error`. The form treats every field through one
6
+ // small contract:
7
+ //
8
+ // field.key // the value key in form.value()
9
+ // field.focus() / blur() // gate input (delegates to the control)
10
+ // field.focused // is this field focused
11
+ // field.update(msg) → [f,cmd]// fold a Msg (delegates to the control)
12
+ // field.value() // current value (subclass)
13
+ // field.setValue(v) // set it (subclass)
14
+ // field.controlView() // render just the control (subclass)
15
+ // field.menuView() // optional overlay (select dropdown); '' default
16
+ // field.wantsEnter() // true if enter belongs to the control right now
17
+ // field.validate() // run SYNC validation, set .error, return it
18
+ // field.view() // label + description + control + error/spinner
19
+ //
20
+ // Validation can be synchronous or asynchronous. If `validate` is an async
21
+ // function it's run by the form through a Cmd, with a spinner shown meanwhile;
22
+ // the sync helpers below (requiredError/syncValidate) are what the form uses for
23
+ // the instant checks, and runValidator() is what it awaits for the async one.
24
+ //
25
+ // Subclasses set `this.control` (a bare-tui component) and implement value(),
26
+ // setValue() and controlView(). The base delegates focus/blur/update to the
27
+ // control and assembles the field's block in view(), styled through `this.theme`
28
+ // (set to the form's theme on construction, or the default for a lone field).
29
+ const { spinner } = require('bare-tui')
30
+ const { defaultTheme } = require('../theme')
31
+
32
+ const FRAME_SETS = { dots: spinner.dots, line: spinner.line, points: spinner.points }
33
+
34
+ // Sentinel for "this field's async validator has never passed for any value".
35
+ const UNCHECKED = Symbol('unchecked')
36
+
37
+ class Field {
38
+ constructor(opts = {}) {
39
+ this.key = opts.name || opts.key || ''
40
+ // The address of this field's value in the (possibly nested) result object.
41
+ // A flat field's path is just [key]; a field inside a nested object section
42
+ // carries the full chain (['address', 'street']). The form walks it to build
43
+ // and read nested values. Set by the form when it flattens nested defs.
44
+ this.path = Array.isArray(opts.path) && opts.path.length ? opts.path.slice() : [this.key]
45
+ // Section toggles (the optional-subform checkbox) set this; leaf fields don't.
46
+ this.isSection = false
47
+ // A oneOf/anyOf variant *selector* sets this: it drives which branch is live
48
+ // but carries no value of its own, so the form skips it when collecting.
49
+ this.isSelector = false
50
+ // Can the focus ring land here? A const (fixed discriminator) field is shown
51
+ // and collected but not interactive, so it opts out.
52
+ this.focusable = true
53
+ // An action button (array add/remove): focusable, carries no value, and the
54
+ // form runs its `action` on enter instead of validating/advancing.
55
+ this.isButton = false
56
+ this.label = opts.label || this.key
57
+ this.description = opts.description || ''
58
+ // Presentation (uiSchema): extra help line, hidden label, read-only display,
59
+ // initial-focus hint, and fully-hidden (collected but never shown) fields.
60
+ this.help = opts.help || ''
61
+ this.hideLabel = !!opts.hideLabel
62
+ this.autofocus = !!opts.autofocus
63
+ this.hidden = !!opts.hidden
64
+ if (opts.readonly || opts.hidden) this.focusable = false
65
+ this.required = !!opts.required
66
+ this.requiredMessage = opts.requiredMessage || 'required'
67
+ this.validatingMessage = opts.validatingMessage || 'checking…'
68
+ this._validate = typeof opts.validate === 'function' ? opts.validate : null
69
+ this._isAsync = !!this._validate && this._validate.constructor.name === 'AsyncFunction'
70
+
71
+ this.error = null
72
+ this.validating = false // true while an async check is in flight
73
+ this.spinner = null // bare-tui spinner, live only while validating
74
+ this._runId = 0 // generation: bumped to discard stale async results
75
+ // The value its async validator last *passed* for; compared to the current
76
+ // value to tell whether an async check still needs running (see the form's
77
+ // validate-on-submit pass). UNCHECKED until the first passing result.
78
+ this._asyncCheckedValue = UNCHECKED
79
+
80
+ this.theme = defaultTheme
81
+ this.control = null // set by the subclass
82
+ }
83
+
84
+ setTheme(theme) {
85
+ this.theme = theme
86
+ return this
87
+ }
88
+
89
+ focus() {
90
+ if (this.control) this.control.focus()
91
+ return this
92
+ }
93
+
94
+ blur() {
95
+ if (this.control) this.control.blur()
96
+ return this
97
+ }
98
+
99
+ get focused() {
100
+ return this.control ? this.control.focused : false
101
+ }
102
+
103
+ update(msg) {
104
+ if (!this.control) return [this, null]
105
+ const [c, cmd] = this.control.update(msg)
106
+ this.control = c
107
+ // Editing clears a stale error; it's re-checked on the next confirm.
108
+ if (this.error) this.error = null
109
+ return [this, cmd]
110
+ }
111
+
112
+ // Most controls don't need enter; textarea (newline) and an open select
113
+ // (commit) do, and override this.
114
+ wantsEnter() {
115
+ return false
116
+ }
117
+
118
+ // Overlay content (the select dropdown). Empty for everything else.
119
+ menuView() {
120
+ return ''
121
+ }
122
+
123
+ // Is `v` "empty" for the purpose of `required`? Overridable (confirm/number).
124
+ isEmpty(v) {
125
+ return v === '' || v === null || v === undefined || (Array.isArray(v) && v.length === 0)
126
+ }
127
+
128
+ // --- validation -----------------------------------------------------------
129
+
130
+ // The required-but-empty error, or null. (sync)
131
+ requiredError(v) {
132
+ return this.required && this.isEmpty(v) ? this.requiredMessage : null
133
+ }
134
+
135
+ hasValidator() {
136
+ return !!this._validate
137
+ }
138
+
139
+ isAsync() {
140
+ return this._isAsync
141
+ }
142
+
143
+ // Does this field have an async validator whose current (non-empty) value
144
+ // hasn't passed yet? Used by the form to re-run dirty async checks on submit.
145
+ // Editing changes the value, so a once-passed field goes dirty automatically.
146
+ needsAsyncCheck() {
147
+ if (!this._isAsync) return false
148
+ const v = this.value()
149
+ if (this.isEmpty(v)) return false
150
+ return this._asyncCheckedValue !== v
151
+ }
152
+
153
+ // Run the user validator (may return a string, null, or a Promise of those).
154
+ runValidator(v) {
155
+ return this._validate ? this._validate(v) : null
156
+ }
157
+
158
+ // The instant verdict: required, then the sync validator. Async validators
159
+ // aren't run here (the form awaits them separately); an empty optional field
160
+ // is always fine.
161
+ syncValidate(v) {
162
+ if (this.required && this.isEmpty(v)) return this.requiredMessage
163
+ if (this.isEmpty(v)) return null
164
+ if (this._validate && !this._isAsync) return this._validate(v) || null
165
+ return null
166
+ }
167
+
168
+ // Backwards-compatible sync entry point: set and return .error.
169
+ validate() {
170
+ this.error = this.syncValidate(this.value())
171
+ return this.error
172
+ }
173
+
174
+ // --- spinner lifecycle (driven by the form) -------------------------------
175
+
176
+ // Start the validating spinner and return its initial tick Cmd.
177
+ startSpinner() {
178
+ const cfg = (this.theme && this.theme.spinner) || {}
179
+ const frames = typeof cfg.frames === 'string' ? FRAME_SETS[cfg.frames] : cfg.frames
180
+ this.spinner = spinner.create({ frames, fps: cfg.fps })
181
+ return this.spinner.init()
182
+ }
183
+
184
+ stopSpinner() {
185
+ this.spinner = null
186
+ }
187
+
188
+ // --- rendering ------------------------------------------------------------
189
+
190
+ // label (+ marker when required) · description · control · help · error|spinner.
191
+ // The label is omitted when hideLabel (uiSchema's `ui:label: false`).
192
+ view() {
193
+ const t = this.theme
194
+ const lines = []
195
+ if (!this.hideLabel) {
196
+ const labelText = this.label + (this.required ? t.requiredMarker : '')
197
+ lines.push(this.focused ? t.labelFocused(labelText) : t.label(labelText))
198
+ }
199
+ if (this.description) lines.push(t.description(this.description))
200
+ lines.push(...this.controlView().split('\n'))
201
+ if (this.help) lines.push(t.help(this.help))
202
+
203
+ if (this.validating) {
204
+ const frame = this.spinner ? this.spinner.view() : ''
205
+ lines.push(t.validating((frame ? frame + ' ' : '') + this.validatingMessage))
206
+ } else if (this.error) {
207
+ lines.push(t.error(t.errorPrefix + this.error))
208
+ }
209
+ return lines.join('\n')
210
+ }
211
+ }
212
+
213
+ module.exports = { Field }
@@ -0,0 +1,46 @@
1
+ // confirm — a boolean field, wrapping bare-tui's checkbox.
2
+ //
3
+ // When `required`, "empty" means "not checked", so a required confirm must be
4
+ // ticked to pass (e.g. an "I agree" box).
5
+ const { checkbox } = require('bare-tui')
6
+ const { Field } = require('./base')
7
+
8
+ class ConfirmField extends Field {
9
+ constructor(opts = {}) {
10
+ super(opts)
11
+ // The checkbox line *is* the label, so suppress the separate label row that
12
+ // every other field renders above its control — otherwise the text shows
13
+ // twice ("Accept terms" then "[ ] Accept terms").
14
+ this.hideLabel = true
15
+ // The checkbox draws just the pointer + box; the label is added (themed) in
16
+ // controlView so it picks up labelFocused and the required marker.
17
+ this.control = checkbox.create({ label: '', checked: !!opts.value })
18
+ }
19
+
20
+ value() {
21
+ return this.control.checked
22
+ }
23
+
24
+ setValue(v) {
25
+ this.control.setChecked(v)
26
+ return this
27
+ }
28
+
29
+ isEmpty(v) {
30
+ return v !== true
31
+ }
32
+
33
+ // "› [x] Accept terms *" — the box from the checkbox, the label themed here.
34
+ controlView() {
35
+ const t = this.theme
36
+ const text = this.label + (this.required ? t.requiredMarker : '')
37
+ const styled = this.focused ? t.labelFocused(text) : t.label(text)
38
+ return this.control.view() + ' ' + styled
39
+ }
40
+ }
41
+
42
+ function confirm(opts) {
43
+ return new ConfirmField(opts)
44
+ }
45
+
46
+ module.exports = { confirm, ConfirmField }
@@ -0,0 +1,41 @@
1
+ // constant — a fixed value (JSON Schema `const`).
2
+ //
3
+ // A const property carries a value the user can't change — most often the
4
+ // discriminator of a oneOf branch (`type: { const: 'card' }`). It's shown as a
5
+ // static line and included in the collected value, but it's not interactive, so
6
+ // it opts out of the focus ring (`focusable = false`) and never errors.
7
+ const { Field } = require('./base')
8
+
9
+ class ConstField extends Field {
10
+ constructor(opts = {}) {
11
+ super(opts)
12
+ this.focusable = false
13
+ this.constValue = opts.value
14
+ }
15
+
16
+ value() {
17
+ return this.constValue
18
+ }
19
+
20
+ setValue() {
21
+ return this // fixed — rehydration can't change a const
22
+ }
23
+
24
+ isEmpty() {
25
+ return false
26
+ }
27
+
28
+ syncValidate() {
29
+ return null
30
+ }
31
+
32
+ controlView() {
33
+ return this.constValue === undefined ? '' : String(this.constValue)
34
+ }
35
+ }
36
+
37
+ function constant(opts) {
38
+ return new ConstField(opts)
39
+ }
40
+
41
+ module.exports = { constant, ConstField }
@@ -0,0 +1,97 @@
1
+ // multiselect — choose many from a list of toggles.
2
+ //
3
+ // bare-tui has no checkbox *group*, so this field implements its own little
4
+ // control inline (cursor + a checked set) rather than wrapping one. It still
5
+ // presents the standard Field contract, so the form treats it like any other.
6
+ // value() is an array of the checked values, in option order.
7
+ const { key } = require('bare-tui')
8
+ const { Field } = require('./base')
9
+
10
+ const keys = {
11
+ up: key.binding({ keys: ['up', 'k'] }),
12
+ down: key.binding({ keys: ['down', 'j'] }),
13
+ toggle: key.binding({ keys: ['space'] })
14
+ }
15
+
16
+ function normalize(opt) {
17
+ if (opt !== null && typeof opt === 'object') {
18
+ const value = 'value' in opt ? opt.value : opt.label
19
+ return { label: String(opt.label ?? opt.value ?? ''), value }
20
+ }
21
+ return { label: String(opt), value: opt }
22
+ }
23
+
24
+ class MultiSelectField extends Field {
25
+ constructor(opts = {}) {
26
+ super(opts)
27
+ this.options = (opts.options || []).map(normalize)
28
+ this.cursor = 0
29
+ this.checked = new Set() // indices
30
+ this._focused = false
31
+ this.onGlyph = opts.onGlyph || '[x]'
32
+ this.offGlyph = opts.offGlyph || '[ ]'
33
+ if (Array.isArray(opts.value)) this.setValue(opts.value)
34
+ // No wrapped control; we override the delegating methods below.
35
+ this.control = null
36
+ }
37
+
38
+ focus() {
39
+ this._focused = true
40
+ return this
41
+ }
42
+
43
+ blur() {
44
+ this._focused = false
45
+ return this
46
+ }
47
+
48
+ get focused() {
49
+ return this._focused
50
+ }
51
+
52
+ value() {
53
+ const out = []
54
+ this.options.forEach((o, i) => {
55
+ if (this.checked.has(i)) out.push(o.value)
56
+ })
57
+ return out
58
+ }
59
+
60
+ setValue(values) {
61
+ const want = new Set(values || [])
62
+ this.checked = new Set()
63
+ this.options.forEach((o, i) => {
64
+ if (want.has(o.value)) this.checked.add(i)
65
+ })
66
+ return this
67
+ }
68
+
69
+ update(msg) {
70
+ if (!this._focused || !msg || msg.type !== 'key') return [this, null]
71
+ if (key.matches(msg, keys.up)) this.cursor = Math.max(0, this.cursor - 1)
72
+ else if (key.matches(msg, keys.down)) {
73
+ this.cursor = Math.min(this.options.length - 1, this.cursor + 1)
74
+ } else if (key.matches(msg, keys.toggle)) {
75
+ if (this.checked.has(this.cursor)) this.checked.delete(this.cursor)
76
+ else this.checked.add(this.cursor)
77
+ if (this.error) this.error = null
78
+ }
79
+ return [this, null]
80
+ }
81
+
82
+ controlView() {
83
+ return this.options
84
+ .map((o, i) => {
85
+ const pointer = this._focused && i === this.cursor ? '› ' : ' '
86
+ const box = this.checked.has(i) ? this.onGlyph : this.offGlyph
87
+ return pointer + box + ' ' + o.label
88
+ })
89
+ .join('\n')
90
+ }
91
+ }
92
+
93
+ function multiselect(opts) {
94
+ return new MultiSelectField(opts)
95
+ }
96
+
97
+ module.exports = { multiselect, MultiSelectField, keys }
@@ -0,0 +1,60 @@
1
+ // number — a numeric field. Wraps textinput for entry, but value() coerces to a
2
+ // Number and syncValidate() enforces numeric-ness plus optional min/max/integer.
3
+ const { textinput } = require('bare-tui')
4
+ const { Field } = require('./base')
5
+
6
+ class NumberField extends Field {
7
+ constructor(opts = {}) {
8
+ super(opts)
9
+ this.min = opts.min
10
+ this.max = opts.max
11
+ this.integer = !!opts.integer
12
+ this.control = textinput.create({
13
+ value: opts.value === undefined || opts.value === null ? '' : String(opts.value),
14
+ placeholder: opts.placeholder || '',
15
+ prompt: opts.prompt ?? '> '
16
+ })
17
+ }
18
+
19
+ // null when blank, a finite Number when parseable, otherwise NaN (so
20
+ // syncValidate() can flag it; an unflagged NaN would JSON-serialize to null).
21
+ value() {
22
+ const raw = this.control.value.trim()
23
+ if (raw === '') return null
24
+ return Number(raw)
25
+ }
26
+
27
+ setValue(v) {
28
+ this.control.setValue(v === undefined || v === null ? '' : String(v))
29
+ return this
30
+ }
31
+
32
+ isEmpty(v) {
33
+ return v === null
34
+ }
35
+
36
+ // The numeric rules live here (not in validate()) because the form validates
37
+ // through syncValidate — both on enter-confirm and on submit.
38
+ syncValidate(v) {
39
+ // Base handles required + empty + any custom validator first.
40
+ const base = super.syncValidate(v)
41
+ if (base) return base
42
+
43
+ if (v === null) return null // optional and blank
44
+ if (Number.isNaN(v)) return 'must be a number'
45
+ if (this.integer && !Number.isInteger(v)) return 'must be a whole number'
46
+ if (this.min !== undefined && v < this.min) return `must be ≥ ${this.min}`
47
+ if (this.max !== undefined && v > this.max) return `must be ≤ ${this.max}`
48
+ return null
49
+ }
50
+
51
+ controlView() {
52
+ return this.control.view()
53
+ }
54
+ }
55
+
56
+ function number(opts) {
57
+ return new NumberField(opts)
58
+ }
59
+
60
+ module.exports = { number, NumberField }
@@ -0,0 +1,32 @@
1
+ // radio — pick one from an expanded list, wrapping bare-tui's radio.
2
+ const { radio } = require('bare-tui')
3
+ const { Field } = require('./base')
4
+
5
+ class RadioField extends Field {
6
+ constructor(opts = {}) {
7
+ super(opts)
8
+ this.control = radio.create({
9
+ options: opts.options || [],
10
+ selected: opts.selected || 0
11
+ })
12
+ }
13
+
14
+ value() {
15
+ return this.control.value()
16
+ }
17
+
18
+ setValue(v) {
19
+ this.control.setValue(v)
20
+ return this
21
+ }
22
+
23
+ controlView() {
24
+ return this.control.view()
25
+ }
26
+ }
27
+
28
+ function radio_(opts) {
29
+ return new RadioField(opts)
30
+ }
31
+
32
+ module.exports = { radio: radio_, RadioField }
@@ -0,0 +1,71 @@
1
+ // section — the include/omit toggle for an optional nested-object section.
2
+ //
3
+ // A non-required object in a schema becomes a sub-section the user can choose to
4
+ // fill in or leave out. This field is its gate: a focusable checkbox rendered as
5
+ // the section's heading. The form:
6
+ //
7
+ // - auto-checks it the moment any field inside the section gets a value,
8
+ // - omits the whole subtree from value() while it's unchecked,
9
+ // - and only validates the section's inner `required` fields while it's on.
10
+ //
11
+ // So the user is in control — start typing and the section is "in", toggle the
12
+ // box off to drop it — and the fields stay visible the whole time (no dynamic
13
+ // show/hide). It never carries a value of its own; `isSection` tells the form to
14
+ // treat it as a gate, not a leaf, and it never blocks submission itself.
15
+ const { checkbox } = require('bare-tui')
16
+ const { Field } = require('./base')
17
+
18
+ class SectionToggleField extends Field {
19
+ constructor(opts = {}) {
20
+ super(opts)
21
+ this.isSection = true
22
+ this.control = checkbox.create({
23
+ label: opts.title || opts.label || this.key,
24
+ checked: !!opts.value
25
+ })
26
+ }
27
+
28
+ get checked() {
29
+ return this.control.checked
30
+ }
31
+
32
+ value() {
33
+ return this.control.checked
34
+ }
35
+
36
+ setValue(v) {
37
+ this.control.setChecked(v)
38
+ return this
39
+ }
40
+
41
+ // A section gate is never itself required and never reports an error — the
42
+ // completeness of an included section is enforced by its own inner fields.
43
+ requiredError() {
44
+ return null
45
+ }
46
+
47
+ syncValidate() {
48
+ return null
49
+ }
50
+
51
+ controlView() {
52
+ return this.control.view()
53
+ }
54
+
55
+ // Renders as the section heading: the checkbox + title, with a quiet hint when
56
+ // it's off so it's clear the section won't be submitted.
57
+ view() {
58
+ const t = this.theme
59
+ const head = t.sectionTitle(this.control.view())
60
+ const hint = this.control.checked ? '' : ' ' + t.help('(off — not included)')
61
+ const lines = [head + hint]
62
+ if (this.description) lines.push(t.description(this.description))
63
+ return lines.join('\n')
64
+ }
65
+ }
66
+
67
+ function section(opts) {
68
+ return new SectionToggleField(opts)
69
+ }
70
+
71
+ module.exports = { section, SectionToggleField }
@@ -0,0 +1,46 @@
1
+ // select — pick one from a dropdown, wrapping bare-tui's select.
2
+ //
3
+ // It forwards the control's overlay (menuView) so the form can composite the
4
+ // dropdown, and claims enter while the menu is open (to commit the choice).
5
+ const { select } = require('bare-tui')
6
+ const { Field } = require('./base')
7
+
8
+ class SelectField extends Field {
9
+ constructor(opts = {}) {
10
+ super(opts)
11
+ this.control = select.create({
12
+ options: opts.options || [],
13
+ selected: opts.selected ?? -1,
14
+ placeholder: opts.placeholder || 'select…',
15
+ maxVisible: opts.maxVisible || 6
16
+ })
17
+ }
18
+
19
+ value() {
20
+ return this.control.value()
21
+ }
22
+
23
+ setValue(v) {
24
+ this.control.setValue(v)
25
+ return this
26
+ }
27
+
28
+ // While the menu is open, enter commits the highlighted option.
29
+ wantsEnter() {
30
+ return this.control.open
31
+ }
32
+
33
+ menuView() {
34
+ return this.control.menuView()
35
+ }
36
+
37
+ controlView() {
38
+ return this.control.view()
39
+ }
40
+ }
41
+
42
+ function select_(opts) {
43
+ return new SelectField(opts)
44
+ }
45
+
46
+ module.exports = { select: select_, SelectField }
package/fields/text.js ADDED
@@ -0,0 +1,35 @@
1
+ // text — a single-line text field, wrapping bare-tui's textinput.
2
+ const { textinput } = require('bare-tui')
3
+ const { Field } = require('./base')
4
+
5
+ class TextField extends Field {
6
+ constructor(opts = {}) {
7
+ super(opts)
8
+ this.control = textinput.create({
9
+ value: opts.value || '',
10
+ placeholder: opts.placeholder || '',
11
+ prompt: opts.prompt ?? '> ',
12
+ charLimit: opts.charLimit || 0,
13
+ echoMode: opts.echoMode || 'normal'
14
+ })
15
+ }
16
+
17
+ value() {
18
+ return this.control.value
19
+ }
20
+
21
+ setValue(v) {
22
+ this.control.setValue(v === null || v === undefined ? '' : v)
23
+ return this
24
+ }
25
+
26
+ controlView() {
27
+ return this.control.view()
28
+ }
29
+ }
30
+
31
+ function text(opts) {
32
+ return new TextField(opts)
33
+ }
34
+
35
+ module.exports = { text, TextField }
@@ -0,0 +1,43 @@
1
+ // textarea — a multi-line text field, wrapping bare-tui's textarea.
2
+ //
3
+ // Unlike the other fields it *captures enter* (newline), so the form moves on
4
+ // via tab rather than enter.
5
+ const { textarea } = require('bare-tui')
6
+ const { Field } = require('./base')
7
+
8
+ class TextareaField extends Field {
9
+ constructor(opts = {}) {
10
+ super(opts)
11
+ this.control = textarea.create({
12
+ value: opts.value || '',
13
+ placeholder: opts.placeholder || '',
14
+ width: opts.width || 40,
15
+ height: opts.rows || opts.height || 4, // `rows` mirrors uiSchema's ui:rows
16
+ charLimit: opts.charLimit || 0
17
+ })
18
+ }
19
+
20
+ value() {
21
+ return this.control.value
22
+ }
23
+
24
+ setValue(v) {
25
+ this.control.setValue(v === null || v === undefined ? '' : v)
26
+ return this
27
+ }
28
+
29
+ // Enter inserts a newline here; advance with tab instead.
30
+ wantsEnter() {
31
+ return true
32
+ }
33
+
34
+ controlView() {
35
+ return this.control.view()
36
+ }
37
+ }
38
+
39
+ function textarea_(opts) {
40
+ return new TextareaField(opts)
41
+ }
42
+
43
+ module.exports = { textarea: textarea_, TextareaField }