implement a autocomplete widget (work in progress)

This commit is contained in:
Andreas Rumpf 2017-10-02 13:53:53 +02:00
commit 57a6fca9d1
3 changed files with 164 additions and 54 deletions

View file

@ -3,38 +3,6 @@
include karaxprelude include karaxprelude
import jstrutils, kdom 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 = proc contentA(): VNode =
result = buildHtml(tdiv): result = buildHtml(tdiv):
text "content A" text "content A"
@ -46,28 +14,6 @@ proc contentB(): VNode =
proc contentC(): VNode = proc contentC(): VNode =
result = buildHtml(tdiv): result = buildHtml(tdiv):
text "content C" 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"])

160
src/autocomplete.nim Normal file
View file

@ -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)

View file

@ -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 startsWith*(a, b: cstring): bool {.importcpp: "startsWith", nodecl.}
proc contains*(a, b: cstring): bool {.importcpp: "(#.indexOf(#)>=0)", 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: int): cstring {.importcpp: "substr", nodecl.}
proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.} proc substr*(s: cstring; start, length: int): cstring {.importcpp: "substr", nodecl.}