From 3d2b8484194100619c8ac19214a75aaec0b0fd8b Mon Sep 17 00:00:00 2001 From: Gabriel Nagy Date: Thu, 29 Feb 2024 12:43:34 +0200 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 validate API to no longer block input if this is the case, thus handing this responsibility to the clients. 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 | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/textinput/textinput.go b/textinput/textinput.go index 24e0b6e2..da3c459e 100644 --- a/textinput/textinput.go +++ b/textinput/textinput.go @@ -181,19 +181,14 @@ 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) + err := m.validateIfDefined(string(runes)) + m.setValueInternal(runes, err) } -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) { + m.Err = err empty := len(m.value) == 0 - m.Err = nil if m.CharLimit > 0 && len(runes) > m.CharLimit { m.value = runes[:m.CharLimit] @@ -307,8 +302,6 @@ func (m *Model) insertRunesFromUserInput(v []rune) { tail := make([]rune, len(tailSrc)) copy(tail, tailSrc) - oldPos := m.pos - // Insert pasted runes for _, r := range paste { head = append(head, r) @@ -323,11 +316,8 @@ func (m *Model) insertRunesFromUserInput(v []rune) { // Put it all back together value := append(head, tail...) - m.setValueInternal(value) - - if m.Err != nil { - m.pos = oldPos - } + inputErr := m.validateIfDefined(string(value)) + m.setValueInternal(value, inputErr) } // If a max width is defined, perform some logic to treat the visible area @@ -378,6 +368,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 +378,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 +424,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 +464,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 +569,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 +591,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() @@ -884,3 +877,10 @@ func (m *Model) previousSuggestion() { m.currentSuggestionIndex = len(m.matchedSuggestions) - 1 } } + +func (m Model) validateIfDefined(v string) error { + if m.Validate != nil { + return m.Validate(v) + } + return nil +}