From b0c720940cf54491a68e0dd13d402663b1e12f41 Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Mon, 4 Sep 2023 20:09:25 +0300 Subject: [PATCH] feat(textinput): make validation customizable This PR builds upon the excellent work in #167 and #114 and adds a bit more customizability to the feature. Currently, the validation API will completely block text input if the Validate function returns an error. This commit makes a breaking change to the ValidateFunc by returning an additonal bool that indicates whether or not input should be blocked. This is helpful for cases where the user is requested to type an existing system path, and the Validate function keeps asserting the existence of the path. With the current implementation such a validation is not possible. For example: > / Err: nil > /t Err: /t: No such file or directory > /tm Err: /tm: No such file or directory > /tmp Err: nil --- textinput/textinput.go | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 82341b97..229dc5ab 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -35,8 +35,9 @@ const ( EchoNone ) -// ValidateFunc is a function that returns an error if the input is invalid. -type ValidateFunc func(string) error +// ValidateFunc is a function that returns an error if the input is invalid and +// a boolean indicating whether text input should be blocked. +type ValidateFunc func(string) (bool, error) // KeyMap is the key bindings for different actions within the textinput. type KeyMap struct { @@ -180,19 +181,17 @@ func (m *Model) SetValue(s string) { // Clean up any special characters in the input provided by the // caller. This avoids bugs due to e.g. tab characters and whatnot. runes := m.san().Sanitize([]rune(s)) - m.setValueInternal(runes) + blockInput, err := m.validateIfDefined(string(runes)) + m.setValueInternal(runes, err, blockInput) } -func (m *Model) setValueInternal(runes []rune) { - if m.Validate != nil { - if err := m.Validate(string(runes)); err != nil { - m.Err = err - return - } +func (m *Model) setValueInternal(runes []rune, err error, blockInput bool) { + m.Err = err + if blockInput { + return } empty := len(m.value) == 0 - m.Err = nil if m.CharLimit > 0 && len(runes) > m.CharLimit { m.value = runes[:m.CharLimit] @@ -323,9 +322,10 @@ func (m *Model) insertRunesFromUserInput(v []rune) { // Put it all back together value := append(head, tail...) - m.setValueInternal(value) + blockInput, inputErr := m.validateIfDefined(string(value)) + m.setValueInternal(value, inputErr, blockInput) - if m.Err != nil { + if blockInput { m.pos = oldPos } } @@ -378,6 +378,7 @@ func (m *Model) handleOverflow() { // deleteBeforeCursor deletes all text before the cursor. func (m *Model) deleteBeforeCursor() { m.value = m.value[m.pos:] + _, m.Err = m.validateIfDefined(string(m.value)) m.offset = 0 m.SetCursor(0) } @@ -387,6 +388,7 @@ func (m *Model) deleteBeforeCursor() { // masked input. func (m *Model) deleteAfterCursor() { m.value = m.value[:m.pos] + _, m.Err = m.validateIfDefined(string(m.value)) m.SetCursor(len(m.value)) } @@ -432,6 +434,7 @@ func (m *Model) deleteWordBackward() { } else { m.value = append(m.value[:m.pos], m.value[oldPos:]...) } + _, m.Err = m.validateIfDefined(string(m.value)) } // deleteWordForward deletes the word right to the cursor. If input is masked @@ -471,6 +474,7 @@ func (m *Model) deleteWordForward() { } else { m.value = append(m.value[:oldPos], m.value[m.pos:]...) } + _, m.Err = m.validateIfDefined(string(m.value)) m.SetCursor(oldPos) } @@ -575,12 +579,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { case tea.KeyMsg: switch { case key.Matches(msg, m.KeyMap.DeleteWordBackward): - m.Err = nil m.deleteWordBackward() case key.Matches(msg, m.KeyMap.DeleteCharacterBackward): m.Err = nil if len(m.value) > 0 { m.value = append(m.value[:max(0, m.pos-1)], m.value[m.pos:]...) + _, m.Err = m.validateIfDefined(string(m.value)) if m.pos > 0 { m.SetCursor(m.pos - 1) } @@ -597,13 +601,12 @@ func (m Model) Update(msg tea.Msg) (Model, tea.Cmd) { if m.pos < len(m.value) { m.SetCursor(m.pos + 1) } - case key.Matches(msg, m.KeyMap.DeleteWordBackward): - m.deleteWordBackward() case key.Matches(msg, m.KeyMap.LineStart): m.CursorStart() case key.Matches(msg, m.KeyMap.DeleteCharacterForward): if len(m.value) > 0 && m.pos < len(m.value) { m.value = append(m.value[:m.pos], m.value[m.pos+1:]...) + _, m.Err = m.validateIfDefined(string(m.value)) } case key.Matches(msg, m.KeyMap.LineEnd): m.CursorEnd() @@ -859,3 +862,10 @@ func (m *Model) previousSuggestion() { m.currentSuggestionIndex = len(m.matchedSuggestions) - 1 } } + +func (m Model) validateIfDefined(v string) (bool, error) { + if m.Validate != nil { + return m.Validate(v) + } + return false, nil +}