diff --git a/experiments/menus.nim b/experiments/menus.nim index 450847b..3f79d83 100644 --- a/experiments/menus.nim +++ b/experiments/menus.nim @@ -3,38 +3,6 @@ include karaxprelude import jstrutils, kdom -var - candidates: seq[cstring] = @[] - selected: cstring - showCandidates: bool - -proc autocomplete(choices: seq[cstring]): VNode = - proc onkeyuplater(ev: Event; n: VNode) = - let v = n.value - if v.len > 0: - candidates.setLen 0 - for c in choices: - if v in c: candidates.add(c) - - let inp = buildHtml: - input(onkeyuplater = onkeyuplater, - #onblur = proc () = showCandidates = false, - onfocus = proc () = showCandidates = true) - - proc select(t: cstring): proc() = - result = proc() = - selected = t - showCandidates = false - inp.text = t - inp.dom.value = t - - result = buildHtml(tdiv): - inp - if showCandidates: # or true: - for candy in candidates: - tdiv(onclick = select(candy)): - text candy - proc contentA(): VNode = result = buildHtml(tdiv): text "content A" @@ -46,28 +14,6 @@ proc contentB(): VNode = proc contentC(): VNode = result = buildHtml(tdiv): text "content C" - autocomplete(@[cstring"ActionScript", - "AppleScript", - "Asp", - "BASIC", - "C", - "C++", - "Clojure", - "COBOL", - "Erlang", - "Fortran", - "Groovy", - "Haskell", - "Java", - "JavaScript", - "Lisp", - "Nim", - "Perl", - "PHP", - "Python", - "Ruby", - "Scala", - "Scheme"]) diff --git a/src/autocomplete.nim b/src/autocomplete.nim new file mode 100644 index 0000000..210ed4a --- /dev/null +++ b/src/autocomplete.nim @@ -0,0 +1,160 @@ + +import karax, karaxdsl, vdom, kdom, jstrutils + +type + AutocompleteState* = ref object + choices, candidates: seq[cstring] + selected, maxMatches: int + showCandidates, controlPressed: bool + +proc newAutocomplete*(choices: seq[cstring]; maxMatches = 5): AutocompleteState = + ## Creates a new state for the autocomplete widget. ``maxMatches`` is the maximum + ## number of elements to show. + AutocompleteState(choices: choices, candidates: @[], + selected: -1, maxMatches: maxMatches, showCandidates: false, + controlPressed: false) + +proc autocomplete*(s: AutocompleteState; onselection: proc(s: cstring)): VNode = + var inp: VNode + + proc commit(ev: Event) = + s.showCandidates = false + onselection(inp.dom.value) + when false: + if inp.dom != nil: + echo "setting to A ", inp.dom.value.isNil + result.text = inp.dom.value + else: + echo "setting to B ", inp.text.isNil + result.text = inp.text + for e in result.events: + if e[0] == EventKind.onchange: + e[1](ev, result) + + proc onkeyuplater(ev: Event; n: VNode) = + if not s.controlPressed: + let v = n.value + s.candidates.setLen 0 + for c in s.choices: + if v.len == 0 or c.containsIgnoreCase(v): s.candidates.add(c) + + proc onkeydown(ev: Event; n: VNode) = + const + LEFT = 37 + UP = 38 + RIGHT = 39 + DOWN = 40 + TAB = 9 + ESC = 27 + ENTER = 13 + # UP: Move focus to the previous item. If on first item, move focus to the input. + # If on the input, move focus to last item. + # DOWN: Move focus to the next item. If on last item, move focus to the input. + # If on the input, move focus to the first item. + # ESCAPE: Close the menu. + # ENTER: Select the currently focused item and close the menu. + # TAB: Select the currently focused item, close the menu, and + # move focus to the next focusable element + s.controlPressed = false + case ev.keyCode + of UP: + s.controlPressed = true + s.showCandidates = true + if s.selected > 0: + dec s.selected + n.setInputText s.candidates[s.selected] + of DOWN: + s.controlPressed = true + s.showCandidates = true + if s.selected < s.candidates.len - 1: + inc s.selected + n.setInputText s.candidates[s.selected] + of ESC: + s.showCandidates = false + s.controlPressed = true + of ENTER: + s.controlPressed = true +# inp.setInputText s.choices[i] + commit(ev) + else: + discard + + proc window(s: AutocompleteState): (int, int) = + var first, last: int + if s.selected >= 0: + first = s.selected - (s.maxMatches div 2) + last = s.selected + (s.maxMatches div 2) + if first < 0: first = 0 + # too few because we had to trim first? + if (last - first + 1) < s.maxMatches: last = first + s.maxMatches - 1 + else: + first = 0 + last = s.maxMatches - 1 + if last > high(s.candidates): last = high(s.candidates) + # still too few because we're at the end? + if (last - first + 1) < s.maxMatches: + first = last - s.maxMatches + 1 + if first < 0: first = 0 + + result = (first, last) + + inp = buildHtml: + input(onkeyuplater = onkeyuplater, + onkeydown = onkeydown, + # onblur = proc (ev: Event; n: VNode) = commit(ev), + onfocus = proc (ev: Event; n: VNode) = + onkeyuplater(ev, n) + s.showCandidates = true) + + proc select(i: int): proc(ev: Event; n: VNode) = + result = proc(ev: Event; n: VNode) = + s.selected = i + s.showCandidates = false + inp.setInputText s.choices[i] + commit(ev) + + result = buildHtml(table): + tr: + td: + inp + if s.showCandidates: + let (first, last) = window(s) + for i in first..last: + tr: + td(onclick = select(i), class = cstring"button " & + (if i == s.selected: cstring"is-primary" else: "")): + text s.candidates[i] + +when isMainModule: + const suggestions = @[cstring"ActionScript", + "AppleScript", + "Asp", + "BASIC", + "C", + "C++", + "Clojure", + "COBOL", + "Erlang", + "Fortran", + "Groovy", + "Haskell", + "Java", + "JavaScript", + "Lisp", + "Nim", + "Perl", + "PHP", + "Python", + "Ruby", + "Scala", + "Scheme"] + + var s = newAutocomplete(suggestions) + + proc main(): VNode = + result = buildHtml(tdiv): + autocomplete(s, proc (s: cstring) = echo "now ", s)#: + # proc onchange(ev: Event; n: VNode) = + # echo "now selected ", n.kind + + setRenderer(main) diff --git a/src/jstrutils.nim b/src/jstrutils.nim index 507e7a6..a6f2cc9 100644 --- a/src/jstrutils.nim +++ b/src/jstrutils.nim @@ -6,6 +6,10 @@ proc split*(s, sep: cstring; max: int): seq[cstring] {.importcpp, nodecl.} proc startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.} proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", nodecl.} + +proc containsIgnoreCase*(a, b: cstring): bool {. + importcpp: """(#.search(new RegExp(#.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$$\|]/g, "\\$$&") , "i"))>=0)""", nodecl.} + proc substr*(s: cstring; start: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}