bare-tui-form 0.0.0 → 0.0.2
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/README.md +525 -0
- package/fields/action.js +63 -0
- package/fields/base.js +213 -0
- package/fields/confirm.js +46 -0
- package/fields/constant.js +41 -0
- package/fields/file.js +129 -0
- package/fields/list.js +158 -0
- package/fields/multiselect.js +97 -0
- package/fields/number.js +60 -0
- package/fields/radio.js +32 -0
- package/fields/section.js +71 -0
- package/fields/select.js +46 -0
- package/fields/text.js +35 -0
- package/fields/textarea.js +43 -0
- package/form.js +1028 -0
- package/harden.js +173 -0
- package/index.js +86 -0
- package/package.json +52 -1
- package/prompt.js +107 -0
- package/schema.js +620 -0
- package/structural.js +62 -0
- package/theme.js +86 -0
package/fields/number.js
ADDED
|
@@ -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 }
|
package/fields/radio.js
ADDED
|
@@ -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 }
|
package/fields/select.js
ADDED
|
@@ -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 }
|