Skip to content

Commit

Permalink
Add multi select support
Browse files Browse the repository at this point in the history
  • Loading branch information
Adam Manwaring 'Pants committed May 19, 2023
1 parent c2e487d commit 5b4a55e
Show file tree
Hide file tree
Showing 3 changed files with 249 additions and 20 deletions.
83 changes: 76 additions & 7 deletions list/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@ const NotFound = -1
// visible items. The list can be moved up, down by one item of time or an
// entire page (ie: visible size). It keeps track of the current selected item.
type List struct {
items []*interface{}
scope []*interface{}
cursor int // cursor holds the index of the current selected item
size int // size is the number of visible options
start int
Searcher Searcher
items []*interface{}
scope []*interface{}
cursor int // cursor holds the index of the current selected item
size int // size is the number of visible options
start int
Searcher Searcher
multi []int // holds indexes of multiple selected items
allowMulti bool
}

// New creates and initializes a list of searchable items. The items attribute must be a slice type with a
Expand All @@ -49,6 +51,37 @@ func New(items interface{}, size int) (*List, error) {
return &List{size: size, items: values, scope: values}, nil
}

// NewMulti does the same New() but allows multiple item selections
func NewMulti(items interface{}, size int) (*List, error) {
l, err := New(items, size)
if err != nil {
return l, err
}
l.allowMulti = true
l.multi = make([]int, 0)
return l, err
}

// Select adds or removes the current item to a multi selection list.
// If toggle is false the item is added only.
// Otherwise it is added if not found and removed if found.
func (l *List) Select(toggle bool) {
if l.allowMulti {
j := l.Index()
if j >= 0 {
for i, val := range l.multi {
if val == j {
if toggle {
l.multi = append(l.multi[:i], l.multi[i+1:]...)
}
return
}
}
l.multi = append(l.multi, j)
}
}
}

// Prev moves the visible list back one item. If the selected item is out of
// view, the new select item becomes the last visible item. If the list is
// already at the top, nothing happens.
Expand Down Expand Up @@ -201,7 +234,11 @@ func (l *List) CanPageUp() bool {
// Index returns the index of the item currently selected inside the searched list. If no item is selected,
// the NotFound (-1) index is returned.
func (l *List) Index() int {
selected := l.scope[l.cursor]
return l.findIdx(l.cursor)
}

func (l *List) findIdx(idx int) int {
selected := l.scope[idx]

for i, item := range l.items {
if item == selected {
Expand All @@ -212,6 +249,38 @@ func (l *List) Index() int {
return NotFound
}

// Selected returns a list of selected items.
// The list will be empty if the struct is not a multi-select list.
func (l *List) Selected() []interface{} {
result := make([]interface{}, 0)
if !l.allowMulti || len(l.multi) == 0 {
return result
}
for _, i := range l.multi {
result = append(result, *l.items[i])
}
return result
}

// IsSelected return true if the given index is selected.
func (l *List) IsSelected(idx int) bool {
if !l.allowMulti {
return false
}

sIdx := l.findIdx(idx)
if sIdx < 0 {
return false
}
for _, i := range l.multi {
if i == sIdx {
return true
}
}

return false
}

// Items returns a slice equal to the size of the list with the current visible
// items and the index of the active item in this list.
func (l *List) Items() ([]interface{}, int) {
Expand Down
94 changes: 94 additions & 0 deletions list/list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,18 @@ func TestListNew(t *testing.T) {
})
}

func TestListNewMulti(t *testing.T) {
t.Run("when items a slice nil", func(t *testing.T) {
l, err := NewMulti([]int{1, 2, 3}, 3)
if err != nil {
t.Errorf("Expected no errors, error %v", err)
}
if !l.allowMulti {
t.Error("List not set to multi-select")
}
})
}

func TestListMovement(t *testing.T) {
letters := []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

Expand Down Expand Up @@ -162,3 +174,85 @@ func castList(list []interface{}) []rune {
}
return result
}

func TestListMultSelectMovement(t *testing.T) {
letters := []rune{'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'}

l, err := New(letters, 4)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
m, err := NewMulti(letters, 4)
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}

tcs := []struct {
mexpect []rune
lexpect []rune
move string
selected rune
action string
}{
{move: "next", action: "", selected: 'b', mexpect: []rune{}, lexpect: []rune{}},
{move: "prev", action: "", selected: 'a', mexpect: []rune{}, lexpect: []rune{}},
{move: "prev", action: "select", selected: 'a', mexpect: []rune{'a'}, lexpect: []rune{}},
{move: "next", action: "", selected: 'b', mexpect: []rune{'a'}, lexpect: []rune{}},
{move: "next", action: "select", selected: 'c', mexpect: []rune{'a', 'c'}, lexpect: []rune{}},
{move: "next", action: "", selected: 'd', mexpect: []rune{'a', 'c'}, lexpect: []rune{}},
{move: "next", action: "select", selected: 'e', mexpect: []rune{'a', 'c', 'e'}, lexpect: []rune{}},
{move: "prev", action: "", selected: 'd', mexpect: []rune{'a', 'c', 'e'}, lexpect: []rune{}},
{move: "up", action: "select", selected: 'a', mexpect: []rune{'c', 'e'}, lexpect: []rune{}},
{move: "up", action: "", selected: 'a', mexpect: []rune{'c', 'e'}, lexpect: []rune{}},
{move: "down", action: "", selected: 'e', mexpect: []rune{'c', 'e'}, lexpect: []rune{}},
{move: "down", action: "select", selected: 'g', mexpect: []rune{'c', 'e', 'g'}, lexpect: []rune{}},
{move: "down", action: "enter", selected: 'j', mexpect: []rune{'c', 'e', 'g', 'j'}, lexpect: []rune{}},
{move: "down", action: "enter", selected: 'j', mexpect: []rune{'c', 'e', 'g', 'j'}, lexpect: []rune{}},
}

for _, tc := range tcs {
t.Run(fmt.Sprintf("list %s", tc.move), func(t *testing.T) {
switch tc.move {
case "next":
l.Next()
m.Next()
case "prev":
l.Prev()
m.Prev()
case "up":
l.PageUp()
m.PageUp()
case "down":
l.PageDown()
m.PageDown()
default:
t.Fatalf("unknown move %q", tc.move)
}

switch tc.action {
case "select":
l.Select(true)
m.Select(true)
case "enter":
l.Select(false)
m.Select(false)
}

list := l.Selected()

got := castList(list)

if !reflect.DeepEqual(tc.lexpect, got) {
t.Errorf("L expected %q, got %q", tc.lexpect, got)
}

list = m.Selected()

got = castList(list)

if !reflect.DeepEqual(tc.mexpect, got) {
t.Errorf("M expected %q, got %q", tc.mexpect, got)
}
})
}
}
Loading

0 comments on commit 5b4a55e

Please sign in to comment.