summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/autocomplete.js
blob: b5e4d5def75904f423cae04b15e3fca04c8c3082 (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
/**
 * Autocomplete.
 */

const cache = {};
let inputField, originalTerm;

function removeParent() {
  const parent = document.querySelector('.autocomplete');
  if (parent) parent.parentNode.removeChild(parent);
}

function removeSelected() {
  const selected = document.querySelector('.autocomplete__item--selected');
  if (selected) selected.classList.remove('autocomplete__item--selected');
}

function changeSelected(firstOrLast, current, sibling) {
  if (current && sibling) { // if the currently selected item has a sibling, move selection to it
    current.classList.remove('autocomplete__item--selected');
    sibling.classList.add('autocomplete__item--selected');
  }
  else if (current) { // if the next keypress will take the user outside the list, restore the unautocompleted term
    inputField.value = originalTerm;
    removeSelected();
  }
  else if (firstOrLast) { // if no item in the list is selected, select the first or last
    firstOrLast.classList.add('autocomplete__item--selected');
  }
}

function keydownHandler(event) {
  const selected = document.querySelector('.autocomplete__item--selected'),
        firstItem = document.querySelector('.autocomplete__item:first-of-type'),
        lastItem = document.querySelector('.autocomplete__item:last-of-type');

  if (event.keyCode === 38) changeSelected(lastItem, selected, selected && selected.previousSibling); // ArrowUp
  if (event.keyCode === 40) changeSelected(firstItem, selected, selected && selected.nextSibling); // ArrowDown
  if (event.keyCode === 13 || event.keyCode === 27 || event.keyCode === 188) removeParent(); // Enter || Esc || Comma
  if (event.keyCode === 38 || event.keyCode === 40) { // ArrowUp || ArrowDown
    const newSelected = document.querySelector('.autocomplete__item--selected');
    if (newSelected) inputField.value = newSelected.dataset.value;
    event.preventDefault();
  }
}

function createItem(list, suggestion) {
  const item = document.createElement('li');
  item.className = 'autocomplete__item';

  item.textContent = suggestion.label;
  item.dataset.value = suggestion.value;

  item.addEventListener('mouseover', () => {
    removeSelected();
    item.classList.add('autocomplete__item--selected');
    inputField.value = item.dataset.value;
  });

  item.addEventListener('mouseout', () => {
    inputField.value = originalTerm;
    removeSelected();
  });

  item.addEventListener('click', () => {
    inputField.dispatchEvent(
      new CustomEvent('autocomplete', {
        detail: {
          type: 'click',
          label: suggestion.label,
          value: suggestion.value,
        }
      })
    );
  });

  list.appendChild(item);
}

function createList(suggestions) {
  const parent = document.querySelector('.autocomplete'),
        list = document.createElement('ul');
  list.className = 'autocomplete__list';

  suggestions.forEach(suggestion => createItem(list, suggestion));

  parent.appendChild(list);
}

function createParent() {
  const parent = document.createElement('div');
  parent.className = 'autocomplete';

  // Position the parent below the inputfield
  parent.style.position = 'absolute';
  parent.style.left = `${inputField.offsetLeft}px`;
  // Take the inputfield offset, add its height and subtract the amount by which the parent element has scrolled
  parent.style.top = `${inputField.offsetTop + inputField.offsetHeight - inputField.parentNode.scrollTop}px`;

  // We append the parent at the end of body
  document.body.appendChild(parent);
}

function showAutocomplete(suggestions, targetInput) {
  // Remove old autocomplete suggestions
  removeParent();

  // Save suggestions in cache
  cache[targetInput.value] = suggestions;

  // If the input target is not empty, still visible, and suggestions were found
  if (targetInput.value && targetInput.style.display !== 'none' && suggestions.length) {
    createParent();
    createList(suggestions);
    inputField.addEventListener('keydown', keydownHandler);
  }
}

function getSuggestions() {
  return fetch(inputField.dataset.acSource + inputField.value).then(response => response.json());
}

function listenAutocomplete() {
  let timeout;

  document.addEventListener('input', event => {
    removeParent();

    window.clearTimeout(timeout);
    // Use a timeout to delay requests until the user has stopped typing
    timeout = window.setTimeout(() => {
      inputField = event.target;
      originalTerm = inputField.value;
      const {ac, acMinLength} = inputField.dataset;

      if (ac && (inputField.value.length >= acMinLength)) {

        if (cache[inputField.value]) {
          showAutocomplete(cache[inputField.value], event.target);
        }
        else {
          // inputField could get overwritten while the suggestions are being fetched - use event.target
          getSuggestions().then(suggestions => showAutocomplete(suggestions, event.target));
        }

      }
    }, 300);
  });

  // If there's a click outside the inputField, remove autocomplete
  document.addEventListener('click', event => {
    if (event.target && event.target !== inputField) removeParent();
  });
}

export { listenAutocomplete };