UNPKG

12.4 kBJavaScriptView Raw
1
2import { Dropdown, DropdownUI, DropdownConfig } from './Dropdown';
3import {
4 addEventOnce,
5 addEvent,
6 removeEvent,
7 parseTemplate
8} from './utils/DOM';
9import {
10 getActionObject,
11 pushCallbackToElement,
12 callElementCallbacks,
13 initObjectExtensions
14} from './utils/core';
15
16
17
18export const AutocompleteConfig = Object.assign({}, DropdownConfig, {
19
20 // override
21 useTagNames: true,
22
23 tagName: 'autocomplete',
24
25 // custom
26 delay: 200,
27 minChar: 2,
28 showMark: false,
29 allowCustomInput: false,
30 classNameNotFound: 'dropdown-header',
31 textNotFound: 'No results found',
32
33});
34
35
36
37export const AutocompleteUI = Object.assign({}, DropdownUI, {
38
39 Config: AutocompleteConfig,
40
41 getInput(autocomplete) {
42 return autocomplete.querySelector('input:not([type="hidden"])') || false;
43 },
44
45 getHiddenInput(autocomplete) {
46 return autocomplete.querySelector('input[type="hidden"]') || false;
47 },
48
49 getTriggerElement(autocomplete) {
50 return this.getInput(autocomplete);
51 },
52
53 applyTemplateToMenuItem(item, data, templateId) {
54 item.appendChild(parseTemplate(templateId, data));
55 return item;
56 },
57
58 getItemLabel(item) {
59 const label = item.querySelector('[autocompletelabel') || false;
60 if (label) {
61 return label.textContent;
62 }
63 return item.textContent;
64 },
65
66 getTemplateSelectLabel(autocomplete) {
67 const label = autocomplete.getAttribute('selectedlabel');
68 if (label) {
69 return document.getElementById(label);
70 }
71 return false;
72 }
73
74});
75
76
77
78export const Autocomplete = Object.assign({}, Dropdown, {
79
80 UI: AutocompleteUI,
81 Config: AutocompleteConfig,
82
83 // override methods
84
85 init(autocomplete) {
86 if (autocomplete.__bunny_autocomplete !== undefined) {
87 return false;
88 }
89 autocomplete.__bunny_autocomplete = {};
90 autocomplete.__bunny_autocomplete_state = this.getCurState(autocomplete);
91 this._addEvents(autocomplete);
92 this._setARIA(autocomplete);
93
94 initObjectExtensions(this, autocomplete);
95
96 return true;
97 },
98
99 _setARIA(autocomplete) {
100 Dropdown._setARIA(autocomplete);
101 const input = this.UI.getInput(autocomplete);
102 input.setAttribute('role', 'combobox');
103 input.setAttribute('aria-autocomplete', 'list');
104 },
105
106 close(autocomplete) {
107 Dropdown.close.call(this, autocomplete);
108 this.UI.removeMenuItems(autocomplete);
109 },
110
111 _addEvents(autocomplete) {
112 const input = this.UI.getInput(autocomplete);
113 this._addEventFocus(autocomplete, input);
114 this.onItemSelect(autocomplete, (selectedItem) => {
115 if (selectedItem === null) {
116 this._selectItem(autocomplete, false);
117 } else {
118 this._selectItem(autocomplete, selectedItem);
119 }
120 //input.focus();
121 });
122 },
123
124 // config methods
125
126 isCustomValueAllowed(autocomplete) {
127 return autocomplete.hasAttribute('custom') || this.Config.allowCustomInput;
128 },
129
130 getCustomItemContentsTemplate(autocomplete) {
131 return autocomplete.getAttribute('template');
132 },
133
134 isMarkDisplayed(autocomplete) {
135 return autocomplete.hasAttribute('mark') || this.Config.showMark;
136 },
137
138 getMinChar(autocomplete) {
139 if (autocomplete.hasAttribute('min')) {
140 return autocomplete.getAttribute('min')
141 } else {
142 return this.Config.minChar;
143 }
144 },
145
146 isNotFoundDisplayed(autocomplete) {
147 return autocomplete.hasAttribute('shownotfound');
148 },
149
150 // events
151
152 _addEventInput(autocomplete, input) {
153 autocomplete.__bunny_autocomplete_input = addEventOnce(input, 'input', () => {
154 if (input.value.length >= this.getMinChar(autocomplete)) {
155 this.update(autocomplete, input.value);
156 } else {
157 this.close(autocomplete);
158 }
159 }, this.Config.delay);
160 },
161
162 _addEventFocus(autocomplete, input) {
163 input.addEventListener('focus', () => {
164 if (autocomplete.__bunny_autocomplete_focus === undefined) {
165 autocomplete.__bunny_autocomplete_focus = true;
166 autocomplete.__bunny_autocomplete_initial_value = input.value;
167 this._addEventFocusOut(autocomplete, input);
168 this._addEventInput(autocomplete, input);
169
170 // make sure if dropdown menu not opened and initiated with .open()
171 // that on Enter hit form is not submitted
172 autocomplete.__bunny_autocomplete_keydown_closed = addEvent(input, 'keydown', (e) => {
173 if (e.keyCode === KEY_SPACE) {
174 e.stopPropagation();
175 }
176 //if (!this.UI.isOpened(autocomplete)) {
177 if (e.keyCode === KEY_ENTER/* && this.isStateChanged(autocomplete)*/) {
178 e.preventDefault();
179 if (input.value.length === 0) {
180 this.clear(autocomplete);
181 } else if (e.target === input && this.isCustomValueAllowed(autocomplete)) {
182 //console.log('autocomplete custom picked');
183 this._selectItem(autocomplete, false);
184 this._callItemSelectCallbacks(autocomplete, null);
185 }
186 }
187 //}
188 });
189 }
190 })
191 },
192
193 _addEventFocusOut(autocomplete, input) {
194 // if after 300ms on focus out
195 // and focus was not switched to menu item via keyboard
196 // then if value is empty -> clear values
197 // else if custom value not allowed but entered -> restore to previous value
198 const k = addEvent(input, 'blur', () => {
199 setTimeout(() => {
200 if (!this.UI.isMenuItem(autocomplete, document.activeElement)) {
201 delete autocomplete.__bunny_autocomplete_focus;
202 removeEvent(input, 'blur', k);
203 removeEvent(input, 'input', autocomplete.__bunny_autocomplete_input);
204 delete autocomplete.__bunny_autocomplete_input;
205 removeEvent(input, 'keydown', autocomplete.__bunny_autocomplete_keydown_closed);
206 delete autocomplete.__bunny_autocomplete_keydown_closed;
207
208 if (input.value.length === 0) {
209 this.clear(autocomplete);
210 } else if (!this.isCustomValueAllowed(autocomplete) && this.isStateChanged(autocomplete)) {
211 this.restoreState(autocomplete);
212 }
213 }
214 }, 300);
215 })
216 },
217
218
219
220 // item events
221
222 _addItemEvents(autocomplete, items) {
223 // [].forEach.call(items.childNodes, item => {
224 // item.addEventListener('click', () => {
225 // this._callItemSelectCallbacks(autocomplete, item);
226 // })
227 // });
228 },
229
230 // public methods
231
232 update(autocomplete, search) {
233 callElementCallbacks(autocomplete, 'autocomplete_before_update', cb => {
234 cb();
235 });
236 const action = getActionObject(autocomplete);
237 action(search).then(data => {
238 //setTimeout(() => {
239 callElementCallbacks(autocomplete, 'autocomplete_update', cb => {
240 const res = cb(data);
241 if (res !== undefined) {
242 data = res;
243 }
244 });
245 if (Object.keys(data).length > 0) {
246 this.close(autocomplete);
247 let items;
248 const templateId = this.getCustomItemContentsTemplate(autocomplete);
249 if (this.isMarkDisplayed(autocomplete)) {
250 items = this.UI.createMenuItems(data, (item, value, content) => {
251 if (templateId) {
252 item = this.UI.applyTemplateToMenuItem(item, data[value], templateId);
253 }
254 const reg = new RegExp('(' + search + ')', 'ig');
255 const html = item.innerHTML.replace(reg, '<mark>$1</mark>');
256 item.innerHTML = html;
257 return item;
258 });
259 } else {
260 if (templateId) {
261 items = this.UI.createMenuItems(data, (item, value, content) => {
262 return this.UI.applyTemplateToMenuItem(item, data[value], templateId);
263 });
264 } else {
265 items = this.UI.createMenuItems(data);
266 }
267 }
268 this._addItemEvents(autocomplete, items);
269 this.UI.setMenuItems(autocomplete, items);
270 this._setARIA(autocomplete);
271 this.open(autocomplete);
272 } else {
273 this.close(autocomplete);
274 if (this.isNotFoundDisplayed(autocomplete)) {
275 this.UI.removeMenuItems(autocomplete);
276 this.UI.getMenu(autocomplete).appendChild(this.createNotFoundElement());
277 this.open(autocomplete);
278 }
279 }
280 //}, 1000);
281 }).catch(e => {
282 this.UI.getMenu(autocomplete).innerHTML = e.message;
283 this.open(autocomplete);
284 callElementCallbacks(autocomplete, 'autocomplete_update', cb => {
285 cb(false, e);
286 });
287 });
288 },
289
290 createNotFoundElement() {
291 const div = document.createElement('div');
292 div.classList.add(this.Config.classNameNotFound);
293 div.textContent = this.Config.textNotFound;
294 return div;
295 },
296
297 onBeforeUpdate(autocomplete, cb) {
298 pushCallbackToElement(autocomplete, 'autocomplete_before_update', cb);
299 },
300
301 onUpdate(autocomplete, cb) {
302 pushCallbackToElement(autocomplete, 'autocomplete_update', cb);
303 },
304
305 restoreState(autocomplete) {
306 const state = this.getState(autocomplete);
307 this.UI.getInput(autocomplete).value = state.label;
308 const hiddenInput = this.UI.getHiddenInput(autocomplete);
309 if (hiddenInput) {
310 hiddenInput.value = state.value;
311 }
312
313 const tplLabel = this.UI.getTemplateSelectLabel(autocomplete);
314 if (tplLabel) {
315 tplLabel.innerHTML = '';
316 }
317
318 this.close(autocomplete);
319 },
320
321 clear(autocomplete) {
322 const input = this.UI.getInput(autocomplete);
323 const hiddenInput = this.UI.getHiddenInput(autocomplete);
324 input.value = '';
325 if (hiddenInput) {
326 hiddenInput.value = '';
327 }
328 autocomplete.__bunny_autocomplete_state = this.getCurState(autocomplete);
329 //this._updateInputValues(autocomplete, false);
330
331 const tplLabel = this.UI.getTemplateSelectLabel(autocomplete);
332 if (tplLabel) {
333 tplLabel.innerHTML = '';
334 }
335
336 this._callItemSelectCallbacks(autocomplete, false);
337 this.close(autocomplete);
338 },
339
340 getValue(autocomplete) {
341 const hiddenInput = this.UI.getHiddenInput(autocomplete);
342 if (hiddenInput) {
343 return hiddenInput.value;
344 } else {
345 return this.UI.getInput(autocomplete).value;
346 }
347 },
348
349 getState(autocomplete) {
350 return autocomplete.__bunny_autocomplete_state;
351 },
352
353 getCurState(autocomplete) {
354 const state = {};
355 const input = this.UI.getInput(autocomplete);
356 const hiddenInput = this.UI.getHiddenInput(autocomplete);
357 state.label = input.value;
358 if (hiddenInput) {
359 state.value = hiddenInput.value;
360 }
361 return state;
362 },
363
364 isStateChanged(autocomplete) {
365 return JSON.stringify(this.getState(autocomplete)) !== JSON.stringify(this.getCurState(autocomplete));
366 },
367
368 // private methods
369
370 _updateInputValues(autocomplete, item = false) {
371 const input = this.UI.getInput(autocomplete);
372
373 if (item !== false) {
374 const val = this.UI.getItemLabel(item);
375 input.value = val;
376 autocomplete.__bunny_autocomplete_initial_value = val;
377 } else {
378 if (this.isCustomValueAllowed(autocomplete)) {
379 autocomplete.__bunny_autocomplete_initial_value = input.value;
380 } else {
381 input.value = '';
382 autocomplete.__bunny_autocomplete_initial_value = '';
383 }
384 }
385
386 const hiddenInput = this.UI.getHiddenInput(autocomplete);
387 if (hiddenInput) {
388 if (item !== false) {
389 hiddenInput.value = this.UI.getItemValue(item);
390 } else {
391 if (this.isCustomValueAllowed(autocomplete)) {
392 hiddenInput.value = input.value;
393 } else {
394 hiddenInput.value = '';
395 }
396 }
397 }
398
399 const tplLabel = this.UI.getTemplateSelectLabel(autocomplete);
400 if (tplLabel) {
401 tplLabel.innerHTML = item === false ? '' : item.innerHTML;
402 }
403
404 autocomplete.__bunny_autocomplete_state = this.getCurState(autocomplete);
405 },
406
407 /**
408 * If item = false, tries to select a custom value;
409 * If custom value not allowed restore initial value (previously selected item or input value attribute otherwise)
410 *
411 * @param {HTMLElement} autocomplete
412 * @param {HTMLElement|false} item
413 * @private
414 */
415 _selectItem(autocomplete, item = false) {
416 if (item === false && !this.isCustomValueAllowed(autocomplete)) {
417 // custom input not allowed, restore to value before input was focused
418 this.restoreState(autocomplete);
419 } else {
420 this._updateInputValues(autocomplete, item);
421 }
422
423 this.close(autocomplete);
424 },
425
426});
427
428document.addEventListener('DOMContentLoaded', () => {
429 Autocomplete.initAll();
430});