Autocomplete.coffee 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. class Autocomplete
  2. constructor: (@getValues, @attrs={}, @onChanged=null) ->
  3. @attrs.oninput = @handleInput
  4. @attrs.onfocus = @handleFocus
  5. @attrs.onblur = @handleBlur
  6. @attrs.onkeydown = @handleKey
  7. @values = []
  8. @selected_index = 0
  9. @focus = false
  10. setNode: (node) =>
  11. @node = node
  12. setValue: (value) ->
  13. @attrs.value = value
  14. if @onChanged
  15. @onChanged(value)
  16. Page.projector.scheduleRender()
  17. filterValues: (filter) =>
  18. current_value = @attrs.value
  19. values = @getValues()
  20. re_highlight = new RegExp("^(.*?)("+filter.split("").join(")(.*?)(")+")(.*?)$", "i")
  21. res = []
  22. for value in values
  23. distance = Text.distance(value, current_value)
  24. if distance != false
  25. # Highlight matched part (every second group)
  26. match = value.match(re_highlight)
  27. if not match then continue
  28. parts = match.map (part, i) ->
  29. if i % 2 == 0
  30. return "<b>#{part}</b>"
  31. else
  32. return part
  33. parts.shift() # First part is full string
  34. res.push([parts.join(""), distance])
  35. res.sort (a,b) ->
  36. return a[1] - b[1]
  37. @values = (row[0] for row in res[0..9])
  38. return @values
  39. renderValue: (node, projector_options, children, attrs) ->
  40. node.innerHTML = attrs.key
  41. handleInput: (e) =>
  42. @attrs.value = e.target.value
  43. @selected_index = 0
  44. @focus = true
  45. handleKey: (e) =>
  46. if e.keyCode == 38 # Up
  47. @selected_index = Math.max(0, @selected_index-1)
  48. return false
  49. else if e.keyCode == 40 # Down
  50. @selected_index = Math.min(@values.length-1, @selected_index+1)
  51. return false
  52. else if e.keyCode == 13 # Enter
  53. @handleBlur(e)
  54. return false
  55. handleClick: (e) =>
  56. e.currentTarget ?= e.explicitOriginalTarget
  57. @attrs.value = e.currentTarget.textContent
  58. if @onChanged
  59. @onChanged(@attrs.value)
  60. @focus = false
  61. Page.projector.scheduleRender()
  62. return false
  63. handleFocus: (e) =>
  64. @selected_index = 0
  65. @focus = true
  66. handleBlur: (e) =>
  67. selected_value = @node.querySelector(".values .value.selected")
  68. if selected_value
  69. @setValue selected_value.textContent
  70. else if @attrs.value
  71. values = @filterValues(@attrs.value)
  72. if values.length > 0
  73. @setValue values[0].replace(/<.*?>/g, "")
  74. else
  75. @setValue ""
  76. else
  77. @setValue ""
  78. @focus = false
  79. render: ->
  80. h("div.Autocomplete", {"afterCreate": @setNode}, [
  81. h("input.to", @attrs)
  82. if @focus and @attrs.value then h("div.values", {"exitAnimation": Animation.slideUp}, [
  83. @filterValues(@attrs.value).map (value, i) =>
  84. h("a.value", {
  85. "href": "#Select+Address", "key": value, "tabindex": "-1", "afterCreate": @renderValue, "onmousedown": @handleClick,
  86. "classes": {"selected": @selected_index == i}
  87. })
  88. ])
  89. ])
  90. window.Autocomplete = Autocomplete