charming 0.1.2 → 0.1.4
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.
- checksums.yaml +4 -4
- data/lib/charming/application.rb +22 -5
- data/lib/charming/cli.rb +3 -3
- data/lib/charming/controller/class_methods.rb +2 -2
- data/lib/charming/controller/command_palette.rb +2 -2
- data/lib/charming/controller/component_dispatching.rb +47 -3
- data/lib/charming/controller/focus.rb +123 -0
- data/lib/charming/controller/focus_management.rb +1 -1
- data/lib/charming/controller/rendering.rb +6 -17
- data/lib/charming/controller/session_state.rb +12 -1
- data/lib/charming/controller.rb +11 -2
- data/lib/charming/database/commands.rb +106 -0
- data/lib/charming/generators/component_generator.rb +1 -1
- data/lib/charming/generators/database_installer.rb +154 -0
- data/lib/charming/generators/model_generator.rb +2 -10
- data/lib/charming/generators/name.rb +1 -1
- data/lib/charming/generators/templates/app/application.template +1 -1
- data/lib/charming/generators/templates/app/layout.template +3 -6
- data/lib/charming/generators/templates/app/view.template +1 -1
- data/lib/charming/generators/templates/component/component.rb.template +1 -1
- data/lib/charming/generators/templates/screen/view.rb.template +1 -1
- data/lib/charming/generators/templates/view/view.rb.template +1 -1
- data/lib/charming/generators/view_generator.rb +1 -1
- data/lib/charming/internal/renderer/differential.rb +13 -5
- data/lib/charming/internal/terminal/tty_backend.rb +22 -2
- data/lib/charming/presentation/component.rb +3 -5
- data/lib/charming/presentation/components/activity_indicator.rb +173 -134
- data/lib/charming/presentation/components/command_palette.rb +94 -96
- data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
- data/lib/charming/presentation/components/empty_state.rb +47 -49
- data/lib/charming/presentation/components/form/builder.rb +52 -54
- data/lib/charming/presentation/components/form/confirm.rb +49 -51
- data/lib/charming/presentation/components/form/field.rb +94 -96
- data/lib/charming/presentation/components/form/input.rb +53 -55
- data/lib/charming/presentation/components/form/note.rb +27 -29
- data/lib/charming/presentation/components/form/select.rb +84 -86
- data/lib/charming/presentation/components/form/textarea.rb +67 -69
- data/lib/charming/presentation/components/form.rb +120 -122
- data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
- data/lib/charming/presentation/components/list.rb +123 -125
- data/lib/charming/presentation/components/markdown.rb +21 -23
- data/lib/charming/presentation/components/modal.rb +46 -48
- data/lib/charming/presentation/components/progressbar.rb +51 -53
- data/lib/charming/presentation/components/spinner.rb +40 -42
- data/lib/charming/presentation/components/table.rb +109 -111
- data/lib/charming/presentation/components/text_area.rb +219 -221
- data/lib/charming/presentation/components/text_input.rb +120 -122
- data/lib/charming/presentation/components/viewport.rb +218 -220
- data/lib/charming/presentation/layout/builder.rb +64 -66
- data/lib/charming/presentation/layout/overlay.rb +48 -50
- data/lib/charming/presentation/layout/pane.rb +129 -118
- data/lib/charming/presentation/layout/rect.rb +19 -16
- data/lib/charming/presentation/layout/screen_layout.rb +47 -42
- data/lib/charming/presentation/layout/split.rb +107 -102
- data/lib/charming/presentation/layout.rb +28 -30
- data/lib/charming/presentation/markdown/render_context.rb +30 -14
- data/lib/charming/presentation/markdown/renderer.rb +302 -79
- data/lib/charming/presentation/markdown/style_config.rb +215 -0
- data/lib/charming/presentation/markdown/syntax_highlighter.rb +58 -59
- data/lib/charming/presentation/markdown.rb +4 -6
- data/lib/charming/presentation/template_view.rb +22 -24
- data/lib/charming/presentation/templates/erb_handler.rb +4 -6
- data/lib/charming/presentation/templates.rb +47 -49
- data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
- data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
- data/lib/charming/presentation/ui/border.rb +24 -26
- data/lib/charming/presentation/ui/border_painter.rb +37 -39
- data/lib/charming/presentation/ui/canvas.rb +59 -61
- data/lib/charming/presentation/ui/style.rb +173 -175
- data/lib/charming/presentation/ui/theme.rb +133 -135
- data/lib/charming/presentation/ui/width.rb +12 -14
- data/lib/charming/presentation/ui.rb +69 -71
- data/lib/charming/presentation/view.rb +110 -105
- data/lib/charming/router.rb +3 -8
- data/lib/charming/runtime.rb +25 -10
- data/lib/charming/version.rb +1 -1
- data/lib/charming.rb +5 -4
- metadata +43 -9
- data/lib/charming/database_commands.rb +0 -103
- data/lib/charming/database_installer.rb +0 -152
- data/lib/charming/focus.rb +0 -121
- data/lib/charming/presentation/markdown/block_renderers.rb +0 -120
- data/lib/charming/presentation/markdown/inline_renderers.rb +0 -68
|
@@ -1,69 +1,67 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
4
|
+
module Components
|
|
5
|
+
class Form
|
|
6
|
+
# Input is a single-line Form field backed by a TextInput widget. The cursor position
|
|
7
|
+
# is persisted in the form's per-field state so the field can be refocused mid-edit.
|
|
8
|
+
class Input < Field
|
|
9
|
+
# *value* is the initial text. *placeholder* is shown when the value is empty.
|
|
10
|
+
# *width* optionally constrains the rendered width. All other options are forwarded
|
|
11
|
+
# to Field (label, required, validate, help, theme).
|
|
12
|
+
def initialize(name, value: "", placeholder: "", width: nil, **options)
|
|
13
|
+
super(name, **options)
|
|
14
|
+
@initial_value = value
|
|
15
|
+
@placeholder = placeholder
|
|
16
|
+
@width = width
|
|
17
|
+
end
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
# Binds the field to the form state, sets the initial value if absent, and initializes
|
|
20
|
+
# the per-field cursor offset to the end of the value.
|
|
21
|
+
def bind(state)
|
|
22
|
+
super
|
|
23
|
+
state[:values][name] = @initial_value if state[:values][name].nil?
|
|
24
|
+
field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
|
|
25
|
+
end
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
27
|
+
# Forwards key events to the underlying TextInput, syncing the value and cursor
|
|
28
|
+
# back into the form state. Returns :handled when the event was consumed.
|
|
29
|
+
def handle_key(event)
|
|
30
|
+
text_input = input
|
|
31
|
+
result = text_input.handle_key(event)
|
|
32
|
+
return nil unless result == :handled
|
|
34
33
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
34
|
+
state[:values][name] = text_input.value
|
|
35
|
+
field_state[:cursor] = text_input.cursor
|
|
36
|
+
:handled
|
|
37
|
+
end
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
private
|
|
41
40
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
# The default value for a freshly-bound field is the *value* passed at construction.
|
|
42
|
+
def default_value
|
|
43
|
+
@initial_value
|
|
44
|
+
end
|
|
46
45
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
# Renders the field as "Label: <text input>".
|
|
47
|
+
def render_control
|
|
48
|
+
"#{label}: #{input.render}"
|
|
49
|
+
end
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
51
|
+
# Builds a fresh TextInput each render, seeded from the current form-state value
|
|
52
|
+
# and the persisted cursor offset.
|
|
53
|
+
def input
|
|
54
|
+
TextInput.new(
|
|
55
|
+
value: value.to_s,
|
|
56
|
+
placeholder: @placeholder,
|
|
57
|
+
width: @width,
|
|
58
|
+
cursor: field_state[:cursor]
|
|
59
|
+
)
|
|
60
|
+
end
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
end
|
|
62
|
+
# Returns the per-field state hash for this field.
|
|
63
|
+
def field_state
|
|
64
|
+
state[:fields][name]
|
|
67
65
|
end
|
|
68
66
|
end
|
|
69
67
|
end
|
|
@@ -1,39 +1,37 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
end
|
|
4
|
+
module Components
|
|
5
|
+
class Form
|
|
6
|
+
# Note is a non-interactive Form field that renders a static string of text. Notes
|
|
7
|
+
# never receive focus, never validate, and store no value — they are presentational
|
|
8
|
+
# only, useful for headings, dividers, or instructional text inside a form.
|
|
9
|
+
class Note < Field
|
|
10
|
+
# *text* is the literal string to render. *name* is unused (defaults to :note) and
|
|
11
|
+
# exists only because the Field base class requires a name.
|
|
12
|
+
def initialize(text, name: :note, theme: nil)
|
|
13
|
+
super(name, theme: theme)
|
|
14
|
+
@text = text
|
|
15
|
+
end
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
# Binds the field to the form state but does not create any per-field storage.
|
|
18
|
+
def bind(state)
|
|
19
|
+
@state = state
|
|
20
|
+
end
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
# Notes are never focusable and therefore excluded from Tab/Enter traversal.
|
|
23
|
+
def focusable?
|
|
24
|
+
false
|
|
25
|
+
end
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
# Notes never produce validation errors.
|
|
28
|
+
def validate
|
|
29
|
+
[]
|
|
30
|
+
end
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
end
|
|
32
|
+
# Returns the literal text, ignoring the *active:* flag (notes have no focus state).
|
|
33
|
+
def render(active: false)
|
|
34
|
+
@text.to_s
|
|
37
35
|
end
|
|
38
36
|
end
|
|
39
37
|
end
|
|
@@ -1,110 +1,108 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
4
|
+
module Components
|
|
5
|
+
class Form
|
|
6
|
+
# Select is a single-choice Form field backed by a List widget. The selected option
|
|
7
|
+
# becomes the field's value; navigation keys (up/down/home/end) cycle through options
|
|
8
|
+
# and Enter has no effect (selection is applied immediately on key release).
|
|
9
|
+
class Select < Field
|
|
10
|
+
# *options* is the array of selectable values. *selected_index* defaults to 0.
|
|
11
|
+
# *option_label* is a callable used to extract the display string (default: `to_s`).
|
|
12
|
+
# All other options are forwarded to Field.
|
|
13
|
+
def initialize(name, options:, selected_index: 0, option_label: :to_s.to_proc, **field_options)
|
|
14
|
+
super(name, **field_options)
|
|
15
|
+
@options = options
|
|
16
|
+
@initial_selected_index = selected_index
|
|
17
|
+
@option_label = option_label
|
|
18
|
+
end
|
|
20
19
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
# Binds the field, then ensures the persisted selection (or initial/derived one) is applied.
|
|
21
|
+
def bind(state)
|
|
22
|
+
super
|
|
23
|
+
ensure_selection
|
|
24
|
+
end
|
|
26
25
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
# Forwards key events to the underlying List, syncing the chosen option index back
|
|
27
|
+
# into the field state. Returns :handled when consumed.
|
|
28
|
+
def handle_key(event)
|
|
29
|
+
selection = list
|
|
30
|
+
result = selection.handle_key(event)
|
|
31
|
+
return nil unless result == :handled
|
|
33
32
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
save_selection(selection.selected_index)
|
|
34
|
+
:handled
|
|
35
|
+
end
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
private
|
|
39
38
|
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
# The options array (used as the source of truth for default value and clamp).
|
|
40
|
+
attr_reader :options
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
# The default value is the option at the clamped initial selected index.
|
|
43
|
+
def default_value
|
|
44
|
+
options[clamped_initial_index]
|
|
45
|
+
end
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
# Renders the field as "Label: <display value>".
|
|
48
|
+
def render_control
|
|
49
|
+
"#{label}: #{display_value}"
|
|
50
|
+
end
|
|
52
51
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
52
|
+
# Returns the stringified value via the configured option label callable.
|
|
53
|
+
def display_value
|
|
54
|
+
value.nil? ? "" : @option_label.call(value)
|
|
55
|
+
end
|
|
57
56
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
# Builds a fresh List each render with the current options, selected index, label
|
|
58
|
+
# callable, and theme.
|
|
59
|
+
def list
|
|
60
|
+
List.new(items: options, selected_index: selected_index, label: @option_label, theme: theme)
|
|
61
|
+
end
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
end
|
|
63
|
+
# Ensures the persisted selection is set, falling back to the field's initial index
|
|
64
|
+
# or the current stored value.
|
|
65
|
+
def ensure_selection
|
|
66
|
+
if field_state.key?(:selected_index)
|
|
67
|
+
save_selection(field_state[:selected_index])
|
|
68
|
+
elsif state[:values].key?(name)
|
|
69
|
+
save_selection(index_for(state[:values][name]) || clamped_initial_index)
|
|
70
|
+
else
|
|
71
|
+
save_selection(clamped_initial_index)
|
|
74
72
|
end
|
|
73
|
+
end
|
|
75
74
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
75
|
+
# Persists the chosen *index* and the corresponding option as the field's value.
|
|
76
|
+
def save_selection(index)
|
|
77
|
+
field_state[:selected_index] = clamp_index(index)
|
|
78
|
+
state[:values][name] = options[field_state[:selected_index]]
|
|
79
|
+
end
|
|
81
80
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
# The currently persisted selected index (or the initial index when unset).
|
|
82
|
+
def selected_index
|
|
83
|
+
field_state[:selected_index] || clamped_initial_index
|
|
84
|
+
end
|
|
86
85
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
# Clamps the initial selected index to the valid range.
|
|
87
|
+
def clamped_initial_index
|
|
88
|
+
clamp_index(@initial_selected_index)
|
|
89
|
+
end
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
# Clamps *index* to the valid range. Returns 0 when there are no options.
|
|
92
|
+
def clamp_index(index)
|
|
93
|
+
return 0 if options.empty?
|
|
95
94
|
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
index.to_i.clamp(0, options.length - 1)
|
|
96
|
+
end
|
|
98
97
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
98
|
+
# Returns the index of *option* in the options array, or nil when absent.
|
|
99
|
+
def index_for(option)
|
|
100
|
+
options.index(option)
|
|
101
|
+
end
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
end
|
|
103
|
+
# Returns the per-field state hash for this field.
|
|
104
|
+
def field_state
|
|
105
|
+
state[:fields][name]
|
|
108
106
|
end
|
|
109
107
|
end
|
|
110
108
|
end
|
|
@@ -1,84 +1,82 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Charming
|
|
4
|
-
module
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
end
|
|
4
|
+
module Components
|
|
5
|
+
class Form
|
|
6
|
+
# Textarea is a multi-line Form field backed by a TextArea widget. The cursor offset,
|
|
7
|
+
# top-visible row, and preferred vertical column are all persisted in the form's
|
|
8
|
+
# per-field state so the field behaves consistently when refocused mid-edit.
|
|
9
|
+
class Textarea < Field
|
|
10
|
+
# *value* is the initial text. *placeholder* is shown when the value is empty.
|
|
11
|
+
# *width* and *height* constrain the rendered area. All other options are forwarded
|
|
12
|
+
# to Field (label, required, validate, help, theme).
|
|
13
|
+
def initialize(name, value: "", placeholder: "", width: nil, height: nil, **options)
|
|
14
|
+
super(name, **options)
|
|
15
|
+
@initial_value = value
|
|
16
|
+
@placeholder = placeholder
|
|
17
|
+
@width = width
|
|
18
|
+
@height = height
|
|
19
|
+
end
|
|
21
20
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
# Binds the field, seeds the initial value, and initializes the cursor/offset state.
|
|
22
|
+
def bind(state)
|
|
23
|
+
super
|
|
24
|
+
state[:values][name] = @initial_value if state[:values][name].nil?
|
|
25
|
+
field_state[:cursor] = state[:values][name].to_s.length unless field_state.key?(:cursor)
|
|
26
|
+
field_state[:offset] ||= 0
|
|
27
|
+
end
|
|
29
28
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
29
|
+
# Forwards key events to the underlying TextArea, syncing the value, cursor, offset,
|
|
30
|
+
# and preferred column back into the form state. Returns :handled when consumed.
|
|
31
|
+
def handle_key(event)
|
|
32
|
+
area = text_area
|
|
33
|
+
result = area.handle_key(event)
|
|
34
|
+
return nil unless result == :handled
|
|
36
35
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
state[:values][name] = area.value
|
|
37
|
+
field_state[:cursor] = area.cursor
|
|
38
|
+
field_state[:offset] = area.offset
|
|
39
|
+
field_state[:preferred_column] = area.preferred_column
|
|
40
|
+
:handled
|
|
41
|
+
end
|
|
43
42
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
# Renders the field with its label on the first line, body lines indented, and
|
|
44
|
+
# optional help/error lines below.
|
|
45
|
+
def render(active: false)
|
|
46
|
+
label_line = "#{active ? ">" : " "} #{label}:"
|
|
47
|
+
label_line = theme.selected.render(label_line) if active
|
|
48
|
+
[label_line, *body_lines, help_line, *error_lines].compact.join("\n")
|
|
49
|
+
end
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
private
|
|
53
52
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
# The default value for a freshly-bound field is the *value* passed at construction.
|
|
54
|
+
def default_value
|
|
55
|
+
@initial_value
|
|
56
|
+
end
|
|
58
57
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
58
|
+
# Renders the multi-line body, indenting each line by two spaces.
|
|
59
|
+
def body_lines
|
|
60
|
+
text_area.render.lines(chomp: true).map { |line| " #{line}" }
|
|
61
|
+
end
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
63
|
+
# Builds a fresh TextArea each render, seeded from the current form-state value and
|
|
64
|
+
# the persisted cursor/offset/preferred_column.
|
|
65
|
+
def text_area
|
|
66
|
+
TextArea.new(
|
|
67
|
+
value: value.to_s,
|
|
68
|
+
placeholder: @placeholder,
|
|
69
|
+
width: @width,
|
|
70
|
+
height: @height,
|
|
71
|
+
cursor: field_state[:cursor],
|
|
72
|
+
offset: field_state[:offset],
|
|
73
|
+
preferred_column: field_state[:preferred_column]
|
|
74
|
+
)
|
|
75
|
+
end
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
end
|
|
77
|
+
# Returns the per-field state hash for this field.
|
|
78
|
+
def field_state
|
|
79
|
+
state[:fields][name]
|
|
82
80
|
end
|
|
83
81
|
end
|
|
84
82
|
end
|