aboutsummaryrefslogtreecommitdiff
path: root/src/lib/autoComplete.ts
blob: b0a79eba5683eed71c4ccad3338df9de7598910f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import { h, Children, concatClassName } from 'lib/h'
import * as Button from 'lib/button'

export function create(
  attrs: object,
  id: string,
  keys: string[],
  renderEntry: (entry: string) => Element,
  onInput: (value: string) => void
): Element {
  const completion = h('div', {})

  const updateCompletion = (target: EventTarget, value: string) => {
    const entries = search(value, keys)
    mountOn(
      completion,
      renderCompletion(
        renderEntry,
        selected => {
          (target as HTMLInputElement).value = selected
          completion.remove
          removeChildren(completion)
          onInput(selected)
        },
        entries
      )
    )
  }

  const input = h('input',
    concatClassName(
      { ...attrs,
        id,
        autocomplete: 'off',
        onfocus: (e: Event) => {
          if (e.target !== null) {
            const target = e.target as HTMLInputElement
            updateCompletion(target, target.value)
          }
        },
        oninput: (e: Event) => {
          if (e.target !== null) {
            const target = e.target as HTMLInputElement
            updateCompletion(target, target.value)
            onInput(target.value)
          }
        }
      },
      'g-AutoComplete__Input'
    )
  ) as HTMLInputElement

  input.addEventListener('blur', (e: MouseEvent) => {
    if (e.relatedTarget === null) {
      removeChildren(completion)
    }
  })

  return h('div',
    { className: 'g-AutoComplete' },
    input,
    completion,
    Button.raw(
      { className: 'g-AutoComplete__Clear',
        type: 'button',
        onclick: () => {
          onInput('')
          input.value = ''
          input.focus()
        }
      },
      'x'
    )
  )
}

function renderCompletion(
  renderEntry: (entry: string) => Element,
  onSelect: (entry: string) => void,
  entries: string[]
): Element {
  return h('div',
    { className: 'g-AutoComplete__Completion' },
    ...entries.map(c =>
      Button.raw(
        { className: 'g-AutoComplete__Entry',
          type: 'button',
          onclick: (e: Event) => {
            e.stopPropagation()
            e.preventDefault()
            onSelect(c)
          }
        },
        renderEntry(c)
      )
    )
  )
}

function search(s: string, xs: string[]): string[] {
  return xs.filter(x => x.includes(s))
}

function mountOn(base: Element, ...children: Element[]) {
  removeChildren(base)
  children.forEach(child => base.appendChild(child))
}

function removeChildren(base: Element) {
  const firstChild = base.firstChild
  if (firstChild !== null) {
    base.removeChild(firstChild)
    removeChildren(base)
  }
}