-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathAutocomplete.js
179 lines (156 loc) · 4.96 KB
/
Autocomplete.js
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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
export default class Autocomplete {
constructor(rootEl, options = {}) {
options = Object.assign({ numOfResults: 10, data: [] }, options);
Object.assign(this, { rootEl, options });
this.timer = null;
this.selectionIndex = -1;
this.init();
}
onQueryChange(query, self) {
// Get data for the dropdown
let {url, data, numOfResults} = self.options;
if (url && query) {
self.getDataAsync(query).then(response => {
data = self.parseUsersData(response.items);
self.updateDropdown(data);
});
}
else {
let results = self.getResults(query, data);
results = results.slice(0, numOfResults);
self.updateDropdown(results);
}
}
/**
* Given an array and a query, return a filtered array based on the query.
*/
getResults(query, data) {
if (!query) return [];
// Filter for matching strings
let results = data.filter((item) => {
return item.text.toLowerCase().includes(query.toLowerCase());
});
return results;
}
updateDropdown(results) {
this.listEl.innerHTML = '';
this.listEl.appendChild(this.createResultsEl(results));
}
createResultsEl(results) {
const fragment = document.createDocumentFragment();
results.forEach((result) => {
const el = document.createElement('li');
Object.assign(el, {
className: 'result',
textContent: result.text,
id: result.value
});
// Pass the value to the onSelect callback
el.addEventListener('click', e => {
this.passInputValueCallback(result);
});
// reset all selected items on mouse hover
el.addEventListener('mouseover', e => {
this.selectionIndex = -1;
this.removeHoverColorOfResults();
});
fragment.appendChild(el);
});
return fragment;
}
createQueryInputEl() {
const inputEl = document.createElement('input');
Object.assign(inputEl, {
type: 'search',
name: 'query',
autocomplete: 'off',
});
if (this.options.url) {
// clear the results list when the 'x' is clicked
inputEl.addEventListener('search', e => {
this.updateDropdown([]);
});
inputEl.addEventListener('keyup', e => {
clearTimeout(this.timer);
if (e.target.value && e.keyCode !== 40 && e.keyCode !== 38) {
// small delay to avoid sending many request when the user types in too fast
this.timer = setTimeout(this.onQueryChange, 300, e.target.value, this);
}
});
}
else {
inputEl.addEventListener('input', event => this.onQueryChange(event.target.value, this));
}
// make sure arrow keys navigation through results is done regardless the type of component
inputEl.addEventListener('keydown', e => {
clearTimeout(this.timer);
// querying the DOM only if I'm moving up/down the results dropdown
if (e.keyCode === 40 || e.keyCode === 38 || e.keyCode === 13) {
const results = [...document.querySelector(`#${this.rootEl.id} .results`).children];
switch (e.keyCode) {
// enter key
case 13: {
const result = results[this.selectionIndex];
if (result) {
this.passInputValueCallback({value: results[this.selectionIndex].id});
}
break;
}
// arrow up
case 38: {
if (this.selectionIndex > 0) {
this.removeHoverColorOfResults();
this.selectionIndex--;
results[this.selectionIndex].classList.add('item__selected');
}
break;
}
// arrow down
case 40: {
if (this.selectionIndex < results.length - 1) {
this.removeHoverColorOfResults();
this.selectionIndex++;
results[this.selectionIndex].classList.add('item__selected');
}
break;
}
}
}
});
return inputEl;
}
removeHoverColorOfResults() {
const results = [...document.querySelector(`#${this.rootEl.id} .results`).children];
results.map(elem => {
elem.classList.remove('item__selected');
return elem;
});
}
async getDataAsync(query) {
const {numOfResults, url} = this.options;
const response = await fetch(`${url}?q=${query}&per_page=${numOfResults}`);
return await response.json();
}
parseUsersData(users) {
if (!users) {
return [];
}
return users.map(user => {
user = { text: user.login, value: user.id };
return user;
});
}
passInputValueCallback(result) {
const { onSelect } = this.options;
if (typeof onSelect === 'function') onSelect(result.value);
}
init() {
// Build query input
this.inputEl = this.createQueryInputEl();
this.rootEl.appendChild(this.inputEl);
// Build results dropdown
this.listEl = document.createElement('ul');
Object.assign(this.listEl, { className: 'results' });
this.rootEl.appendChild(this.listEl);
}
}