1 |
|
2 | import { Dropdown, DropdownUI, DropdownConfig } from './Dropdown';
|
3 | import {
|
4 | addEventOnce,
|
5 | addEvent,
|
6 | removeEvent,
|
7 | parseTemplate
|
8 | } from './utils/DOM';
|
9 | import {
|
10 | getActionObject,
|
11 | pushCallbackToElement,
|
12 | callElementCallbacks,
|
13 | initObjectExtensions
|
14 | } from './utils/core';
|
15 |
|
16 |
|
17 |
|
18 | export const AutocompleteConfig = Object.assign({}, DropdownConfig, {
|
19 |
|
20 |
|
21 | useTagNames: true,
|
22 |
|
23 | tagName: 'autocomplete',
|
24 |
|
25 |
|
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 |
|
37 | export 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 |
|
78 | export const Autocomplete = Object.assign({}, Dropdown, {
|
79 |
|
80 | UI: AutocompleteUI,
|
81 | Config: AutocompleteConfig,
|
82 |
|
83 |
|
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 |
|
121 | });
|
122 | },
|
123 |
|
124 |
|
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 |
|
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 |
|
171 |
|
172 | autocomplete.__bunny_autocomplete_keydown_closed = addEvent(input, 'keydown', (e) => {
|
173 | if (e.keyCode === KEY_SPACE) {
|
174 | e.stopPropagation();
|
175 | }
|
176 |
|
177 | if (e.keyCode === KEY_ENTER) {
|
178 | e.preventDefault();
|
179 | if (input.value.length === 0) {
|
180 | this.clear(autocomplete);
|
181 | } else if (e.target === input && this.isCustomValueAllowed(autocomplete)) {
|
182 |
|
183 | this._selectItem(autocomplete, false);
|
184 | this._callItemSelectCallbacks(autocomplete, null);
|
185 | }
|
186 | }
|
187 |
|
188 | });
|
189 | }
|
190 | })
|
191 | },
|
192 |
|
193 | _addEventFocusOut(autocomplete, input) {
|
194 |
|
195 |
|
196 |
|
197 |
|
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 |
|
221 |
|
222 | _addItemEvents(autocomplete, items) {
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 | },
|
229 |
|
230 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 |
|
414 |
|
415 | _selectItem(autocomplete, item = false) {
|
416 | if (item === false && !this.isCustomValueAllowed(autocomplete)) {
|
417 |
|
418 | this.restoreState(autocomplete);
|
419 | } else {
|
420 | this._updateInputValues(autocomplete, item);
|
421 | }
|
422 |
|
423 | this.close(autocomplete);
|
424 | },
|
425 |
|
426 | });
|
427 |
|
428 | document.addEventListener('DOMContentLoaded', () => {
|
429 | Autocomplete.initAll();
|
430 | });
|