PluginProbe
ElasticPress / 5.0.0
ElasticPress v5.0.0
5.3.5 5.3.4 3.6.5 3.6.6 4.0.0 4.0.1 4.1.0 4.2.0 4.2.1 4.2.2 4.3.0 4.3.1 4.4.0 4.4.1 4.5.0 4.5.1 4.5.2 4.6.0 4.6.1 4.7.0 4.7.1 4.7.2 5.0.0 5.0.1 5.0.2 All 108 releases
elasticpress / assets / js / autosuggest / index.js

index.js in ElasticPress 5.0.0, at assets/js/autosuggest/index.js

872 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /* eslint-disable camelcase, no-underscore-dangle, no-use-before-define */
2
3 /**
4 * WordPress dependencies.
5 */
6 import { applyFilters } from '@wordpress/hooks';
7
8 /**
9 * Internal dependencies.
10 */
11 import './back-compat';
12 import {
13 findAncestorByClass,
14 escapeDoubleQuotes,
15 replaceGlobally,
16 debounce,
17 domReady,
18 generateRequestId,
19 } from '../utils/helpers';
20
21 const { epas } = window;
22
23 // Ensure we have an endpoint URL, or
24 // else this shouldn't happen
25 if (epas.endpointUrl && epas.endpointUrl !== '') {
26 init();
27
28 // Publically expose API
29 window.epasAPI = {
30 hideAutosuggestBox,
31 updateAutosuggestBox,
32 esSearch,
33 buildSearchQuery,
34 };
35 }
36
37 /**
38 * Submit the search form
39 *
40 * @param {Node} input - input element
41 */
42 function submitSearchForm(input) {
43 input.closest('form').submit();
44 }
45
46 /**
47 * Set the expanded aria state on the input
48 *
49 * @param {boolean} haveOptions - whether or not the autosuggest list contains results
50 * @param {Node} input - search input
51 */
52 function toggleInputAria(haveOptions, input) {
53 input.setAttribute('aria-expanded', haveOptions);
54 }
55
56 /**
57 * Set the active descendant aria attribute input
58 *
59 * @param {string} id - id of the currently selected element
60 * @param {Node} input - search input
61 */
62 function setInputActiveDescendant(id, input) {
63 input.setAttribute('aria-activedescendant', id);
64 }
65
66 /**
67 * Take selected item and fill the search input
68 *
69 * @param {Node} input - input element
70 * @param {string} text - new input value
71 */
72 function selectAutosuggestItem(input, text) {
73 input.value = text; // eslint-disable-line no-param-reassign
74 }
75
76 /**
77 * Fires events when autosuggest results are clicked,
78 * and if GA tracking is activated
79 *
80 * @param {object} detail - value to pass on to the Custom Event
81 */
82 function triggerAutosuggestEvent(detail) {
83 const event = new CustomEvent('ep-autosuggest-click', { detail });
84 window.dispatchEvent(event);
85
86 if (
87 detail.searchTerm &&
88 parseInt(epas.triggerAnalytics, 10) === 1 &&
89 typeof gtag === 'function'
90 ) {
91 const action = `click - ${detail.searchTerm}`;
92 // eslint-disable-next-line no-undef
93 gtag('event', action, {
94 event_category: 'EP :: Autosuggest',
95 event_label: detail.url,
96 transport_type: 'beacon',
97 });
98 }
99 }
100
101 /**
102 * Navigate to the selected item, and provides
103 * event hook for JS customizations, like GA
104 *
105 * @param {string} searchTerm - user defined search term
106 * @param {string} url - post url from dataset in search result
107 */
108 function goToAutosuggestItem(searchTerm, url) {
109 const detail = {
110 searchTerm,
111 url,
112 };
113
114 triggerAutosuggestEvent(detail);
115 window.location.href = url;
116 }
117
118 /**
119 * Respond to an item selection based on the predefined behavior.
120 * If epas.action is set to "navigate" (the default), redirects the browser to the URL of the selected item
121 * If epas.action is set to any other value (such as "search"), fill in the value and perform the search
122 *
123 * @param {Node} input - search input
124 * @param {Node} element - search term result item
125 * @returns {Function} calls the submitSearchForm function
126 */
127 function selectItem(input, element) {
128 if (epas.action === 'navigate') {
129 /**
130 * Allow to replace the callback function used when navigating an Autosuggest item.
131 *
132 * @filter ep.Autosuggest.navigateCallback
133 * @since 4.5.1
134 *
135 * @param {Function} goToAutosuggestItem Autosuggest Callback.
136 * @returns {Function} Autosuggest Callback
137 */
138 const navigateCallback = applyFilters(
139 'ep.Autosuggest.navigateCallback',
140 goToAutosuggestItem,
141 );
142
143 return navigateCallback(input.value, element.dataset.url);
144 }
145
146 selectAutosuggestItem(input, element.innerText);
147 return submitSearchForm(input);
148 }
149
150 /**
151 * Build the search query from the search text - the query is generated in PHP
152 * and passed into the front end as window.epas = { "query...
153 *
154 * @returns {string} json string
155 */
156 function getJsonQuery() {
157 if (typeof window.epas === 'undefined') {
158 const error = 'No epas object defined';
159
160 // eslint-disable-next-line no-console
161 console.warn(error);
162 return { error };
163 }
164
165 return window.epas;
166 }
167
168 /**
169 * Build the search query from the search text
170 *
171 * @param {string} searchText - user search string
172 * @param {string} placeholder - placeholder text to replace
173 * @param {object} options - Autosuggest settings
174 * @param {string} options.query - JSON query string to pass to ElasticSearch
175 * @returns {string} json representation of search query
176 */
177 function buildSearchQuery(searchText, placeholder, { query }) {
178 const newQuery = replaceGlobally(query, placeholder, searchText);
179 return newQuery;
180 }
181
182 /**
183 * Build the ajax request
184 *
185 * @param {string} query - json string
186 * @param {string} searchTerm - user search term
187 * @returns {object} AJAX object request
188 */
189 async function esSearch(query, searchTerm) {
190 const fetchOptions = {
191 body: query,
192 method: 'POST',
193 mode: 'cors',
194 headers: {
195 'Content-Type': 'application/json; charset=utf-8',
196 },
197 };
198
199 if (epas?.http_headers && typeof epas.http_headers === 'object') {
200 Object.keys(epas.http_headers).forEach((name) => {
201 fetchOptions.headers[name] = epas.http_headers[name];
202 });
203 }
204
205 // only applies headers if using ep.io endpoint
206 if (epas.addSearchTermHeader) {
207 fetchOptions.headers['EP-Search-Term'] = encodeURI(searchTerm);
208 }
209
210 // only add a request ID if using ep.io endpoint
211 const requestId = generateRequestId(epas?.requestIdBase || '');
212 if (requestId) {
213 fetchOptions.headers['X-ElasticPress-Request-ID'] = requestId;
214 }
215
216 try {
217 /**
218 * Filter the Elasticsearch fetch options used for Autosuggest.
219 *
220 * @filter ep.Autosuggest.fetchOptions
221 * @since 4.5.1
222 *
223 * @param {object} fetchOptions Options.
224 * @returns {object} Options.
225 */
226 const response = await fetch(
227 epas.endpointUrl,
228 applyFilters('ep.Autosuggest.fetchOptions', fetchOptions),
229 );
230
231 if (!response.ok) {
232 throw Error(response.statusText);
233 }
234
235 const data = await response.json();
236
237 /**
238 * Filter the Elasticsearch response data used for Autosuggest.
239 *
240 * @filter ep.Autosuggest.data
241 * @since 4.3.1
242 *
243 * @param {object} data Response data.
244 * @param {string} searchTerm Search term.
245 * @returns {object} Response data.
246 */
247 return applyFilters('ep.Autosuggest.data', data, searchTerm);
248 } catch (error) {
249 // eslint-disable-next-line no-console
250 console.error(error);
251 return error;
252 }
253 }
254
255 /**
256 * Update the auto suggest box with new options or hide if none
257 *
258 * @param {Array} options - search results
259 * @param {string} input - search string
260 * @returns {boolean} return true
261 */
262 function updateAutosuggestBox(options, input) {
263 let listHTML = '';
264
265 // get the search term for use later on
266 const { value: searchText } = input;
267 const container = findAncestorByClass(input, 'ep-autosuggest-container');
268 const resultsContainer = container.querySelector('.ep-autosuggest');
269 const suggestList = resultsContainer.querySelector('.autosuggest-list');
270
271 // empty the the list of all child nodes
272 while (suggestList.firstChild) {
273 suggestList.removeChild(suggestList.firstChild);
274 }
275
276 if (options.length > 0) {
277 resultsContainer.style = 'display: block;';
278 } else {
279 resultsContainer.style = 'display: none;';
280 }
281
282 // anticipating the future... a setting where we configure
283 // a limit of results to show, and optionally append a
284 // link to "all results" or something of that nature
285 const resultsLimit = options.length;
286
287 // create markup for list items
288 // eslint-disable-next-line
289 for ( let index = 0; resultsLimit > index; ++index ) {
290 const option = options[index];
291 const text = option._source.post_title;
292 const url = option._source.permalink;
293 const escapedText = escapeDoubleQuotes(text);
294
295 const searchParts = searchText.trim().split(' ');
296 let resultsText = escapedText;
297
298 if (epas.highlightingEnabled) {
299 // uses some regex magic to match upper/lower/capital case
300 const regex = new RegExp(`\\b(${searchParts.join('|')})`, 'gi');
301 resultsText = resultsText.replace(
302 regex,
303 (word) =>
304 `<${epas.highlightingTag} class="${epas.highlightingClass} ep-autosuggest-highlight">${word}</${epas.highlightingTag}>`,
305 );
306 }
307
308 let itemHTML = `<li class="autosuggest-item" role="option" aria-selected="false" id="autosuggest-option-${index}">
309 <a href="${url}" class="autosuggest-link" data-search="${escapedText}" data-url="${url}" tabindex="-1">
310 ${resultsText}
311 </a>
312 </li>`;
313
314 /**
315 * Filter the HTML for an Autosuggest suggestion.
316 *
317 * @filter ep.Autosuggest.itemHTML
318 * @since 4.3.1
319 *
320 * @param {string} itemHTML Item HTML.
321 * @param {object} option Elasticsearch record for suggestion.
322 * @param {number} index Suggestion index.
323 * @param {string} searchText Search term.
324 * @returns {string} Item HTML.
325 */
326 itemHTML = applyFilters('ep.Autosuggest.itemHTML', itemHTML, option, index, searchText);
327
328 listHTML += itemHTML;
329 }
330
331 /**
332 * Filter the HTML for the list of Autosuggest suggestions.
333 *
334 * @filter ep.Autosuggest.listHTML
335 * @since 4.3.1
336 *
337 * @param {string} listHTML List HTML.
338 * @param {object[]} options Elasticsearch records for suggestions.
339 * @param {Element} input Input element that triggered Autosuggest.
340 * @returns {string} List HTML.
341 */
342 listHTML = applyFilters('ep.Autosuggest.listHTML', listHTML, options, input);
343
344 // append list items to the list
345 suggestList.innerHTML = listHTML;
346
347 suggestList.addEventListener('click', (event) => {
348 event.preventDefault();
349
350 const element = event.target.closest('.autosuggest-link');
351
352 if (suggestList.contains(element)) {
353 selectItem(input, element);
354 }
355 });
356
357 return true;
358 }
359
360 /**
361 * Hide the auto suggest box
362 *
363 * @returns {boolean} returns true
364 */
365 function hideAutosuggestBox() {
366 const lists = document.querySelectorAll('.autosuggest-list');
367 const containers = document.querySelectorAll('.ep-autosuggest');
368
369 // empty all EP results lists
370 lists.forEach((list) => {
371 while (list.firstChild) {
372 list.removeChild(list.firstChild);
373 }
374 });
375
376 // hide all EP results containers
377 containers.forEach((container) => {
378 // eslint-disable-next-line
379 container.style = 'display: none;';
380 });
381
382 return true;
383 }
384
385 /**
386 * Checks for any manually ordered posts and puts them in the correct place
387 *
388 * @param {Array} hits - ES results
389 * @param {string} searchTerm - user search term
390 * @returns {object} formatted hits
391 */
392 function checkForOrderedPosts(hits, searchTerm) {
393 const toInsert = {};
394 const taxName = 'ep_custom_result';
395 const lowerCaseSearchTerm = searchTerm.toLowerCase();
396
397 const filteredHits = hits.filter((hit) => {
398 // Should we retain this hit in its current position?
399 let retain = true;
400
401 if (undefined !== hit._source.terms && undefined !== hit._source.terms[taxName]) {
402 hit._source.terms[taxName].forEach((currentTerm) => {
403 if (currentTerm.name.toLowerCase() === lowerCaseSearchTerm) {
404 toInsert[currentTerm.term_order] = hit;
405
406 retain = false;
407 }
408 });
409 }
410
411 return retain;
412 });
413
414 const orderedInserts = {};
415
416 Object.keys(toInsert)
417 .sort()
418 .forEach((key) => {
419 orderedInserts[key] = toInsert[key];
420 });
421
422 if (Object.keys(orderedInserts).length > 0) {
423 Object.keys(orderedInserts).forEach((key) => {
424 const insertItem = orderedInserts[key];
425
426 filteredHits.splice(key - 1, 0, insertItem);
427 });
428 }
429
430 return filteredHits;
431 }
432
433 /**
434 * Add class to the form element while suggestions are being loaded
435 *
436 * @param {boolean} isLoading - whether suggestions are loading
437 * @param {Node} input - search input field
438 */
439 function setFormIsLoading(isLoading, input) {
440 const form = input.closest('form');
441
442 if (isLoading) {
443 form.classList.add('is-loading');
444 } else {
445 form.classList.remove('is-loading');
446 }
447 }
448
449 /**
450 * init method called if the epas endpoint is defined
451 */
452 function init() {
453 const selectors = [epas.defaultSelectors, epas.selector].filter(Boolean).join(',');
454
455 if (!selectors) {
456 return;
457 }
458
459 // For the Autosuggest element that will be cloned.
460 let autosuggestElement;
461
462 // to be used by the handleUpDown function
463 // to keep track of the currently selected result
464 let currentIndex;
465
466 // these are the keycodes we listen for in handleUpDown,
467 // and in handleKeyup
468 const keyCodes = [
469 38, // up
470 40, // down
471 13, // enter
472 ];
473
474 /**
475 * Handles keyup event on the search input
476 *
477 * @param {event} event - keyup event
478 */
479 const handleUpDown = (event) => {
480 if (!keyCodes.includes(event.keyCode)) {
481 return;
482 }
483
484 const input = event.target;
485 const container = findAncestorByClass(input, 'ep-autosuggest-container');
486 const suggestList = container.querySelector('.autosuggest-list');
487 const results = suggestList.children;
488
489 /**
490 * helper function to get the currently selected result
491 *
492 * @returns {number} index of the selected search result
493 */
494 const getSelectedResultIndex = () => {
495 const resultsArr = Array.from(results);
496 return resultsArr.findIndex((result) => result.classList.contains('selected'));
497 };
498
499 /**
500 * helper function to deselect results
501 */
502 const deSelectResults = () => {
503 Array.from(results).forEach((result) => {
504 result.classList.remove('selected');
505 result.setAttribute('aria-selected', 'false');
506 });
507 };
508
509 /**
510 * helper function to selected the next result
511 */
512 const selectNextResult = () => {
513 if (currentIndex >= 0) {
514 const el = results[currentIndex];
515 el.classList.add('selected');
516 el.setAttribute('aria-selected', 'true');
517 setInputActiveDescendant(el.id, input);
518 }
519 };
520
521 // select next or previous based on keyCode
522 // if enter, navigate to that element
523 switch (event.keyCode) {
524 case 38: // Up
525 // don't go less than the 0th index
526 currentIndex = currentIndex - 1 >= 0 ? currentIndex - 1 : 0;
527 deSelectResults();
528 break;
529 case 40: // Down
530 if (typeof currentIndex === 'undefined') {
531 // index is not yet defined, so let's
532 // start with the first one
533 currentIndex = 0;
534 } else {
535 const current = getSelectedResultIndex();
536
537 // check for existence of next result
538 if (results[current + 1]) {
539 currentIndex = current + 1;
540 deSelectResults();
541 }
542 }
543 break;
544 case 13: // Enter
545 if (results[currentIndex]?.classList.contains('selected')) {
546 // navigate to the item defined in the span's data-url attribute
547 selectItem(input, results[currentIndex].querySelector('.autosuggest-link'));
548 }
549 break;
550 default:
551 // No item selected
552 break;
553 }
554
555 // only check next element if up and down key pressed
556 if (results[currentIndex] && results[currentIndex].classList.contains('autosuggest-item')) {
557 selectNextResult();
558 } else {
559 deSelectResults();
560 }
561
562 // keep cursor from heading back to the beginning in the input
563 if (event.keyCode === 38) {
564 // return false;
565 event.preventDefault();
566 }
567 };
568
569 /**
570 * Get the searched post types from the search form.
571 *
572 * @param {HTMLFormElement} form - form containing the search input field
573 * @returns {Array} - post types
574 * @since 3.6.0
575 */
576 function getPostTypesFromForm(form) {
577 const data = new FormData(form);
578
579 if (data.has('post_type')) {
580 return data.getAll('post_type').slice(-1);
581 }
582
583 if (data.has('post_type[]')) {
584 return data.getAll('post_type[]');
585 }
586
587 return [];
588 }
589
590 /**
591 * Calls the ajax request, and outputs the results.
592 * Called by the handleKeyup callback, debounced.
593 *
594 * @param {Node} input - search input field
595 */
596 const fetchResults = async (input) => {
597 // retrieves the PHP-genereated query to pass to ElasticSearch
598 const queryJSON = getJsonQuery();
599
600 if (queryJSON.error) {
601 return;
602 }
603
604 const searchText = input.value;
605 const placeholder = 'ep_autosuggest_placeholder';
606 const postTypes = getPostTypesFromForm(input.form);
607
608 if (searchText.length >= 2) {
609 setFormIsLoading(true, input);
610
611 let query = buildSearchQuery(searchText, placeholder, queryJSON);
612
613 query = JSON.parse(query);
614
615 if (postTypes.length > 0) {
616 if (typeof query.post_filter.bool.must !== 'undefined') {
617 query.post_filter.bool.must.push({
618 terms: {
619 'post_type.raw': postTypes,
620 },
621 });
622 }
623 }
624
625 /**
626 * Filter the Elasticsearch query used for Autosuggest.
627 *
628 * @filter ep.Autosuggest.query
629 * @since 4.3.1
630 *
631 * @param {object} query Elasticsearch query.
632 * @param {string} searchText Search term.
633 * @param {Element} input Input element that triggered Autosuggest.
634 * @returns {object} Elasticsearch query.
635 */
636 query = applyFilters('ep.Autosuggest.query', query, searchText, input);
637 query = JSON.stringify(query);
638
639 // fetch the results
640 const response = await esSearch(query, searchText);
641
642 if (response && response._shards && response._shards.successful > 0) {
643 const hits = checkForOrderedPosts(response.hits.hits, searchText);
644
645 if (hits.length === 0) {
646 hideAutosuggestBox();
647 } else {
648 updateAutosuggestBox(hits, input);
649 }
650 } else {
651 hideAutosuggestBox();
652 }
653
654 setFormIsLoading(false, input);
655 } else if (searchText.length === 0) {
656 hideAutosuggestBox();
657 }
658 };
659
660 const debounceFetchResults = debounce(fetchResults, 200);
661
662 /**
663 * Callback for keyup in Autosuggest container.
664 *
665 * Calls a debounced function to get the search results via
666 * ajax request.
667 *
668 * @param {event} event - keyup event
669 */
670 const handleKeyup = (event) => {
671 event.preventDefault();
672 const { target, key, keyCode } = event;
673
674 if (key === 'Escape' || key === 'Esc' || keyCode === 27) {
675 hideAutosuggestBox();
676 toggleInputAria(false, target);
677 setInputActiveDescendant('', target);
678 return;
679 }
680
681 if (keyCodes.includes(keyCode) && target.value !== '') {
682 handleUpDown(event);
683 return;
684 }
685
686 const input = event.target;
687 debounceFetchResults(input);
688 };
689
690 /**
691 * Wrap an element with an autosuggest container.
692 *
693 * @param {Element} element Element to wrap.
694 * @returns {void}
695 */
696 const wrapInAutosuggestContainer = (element) => {
697 const epContainer = document.createElement('div');
698
699 epContainer.classList.add('ep-autosuggest-container');
700
701 element.insertAdjacentElement('afterend', epContainer);
702
703 epContainer.appendChild(element);
704 };
705
706 /**
707 * Insert an autosuggest list after an element.
708 *
709 * @param {Element} previousElement Element to add the autosuggest list after.
710 * @returns {void}
711 */
712 const insertAutosuggestElement = (previousElement) => {
713 if (!autosuggestElement) {
714 autosuggestElement = document.createElement('div');
715 autosuggestElement.classList.add('ep-autosuggest');
716
717 const autosuggestList = document.createElement('ul');
718
719 autosuggestList.classList.add('autosuggest-list');
720 autosuggestList.setAttribute('role', 'listbox');
721
722 autosuggestElement.appendChild(autosuggestList);
723 }
724
725 let element = autosuggestElement.cloneNode(true);
726
727 /**
728 * Filter the Autosuggest container element before it is inserted.
729 *
730 * @filter ep.Autosuggest.element
731 * @since 4.3.1
732 *
733 * @param {Element} element Autosuggest container element.
734 * @param {Element} previousElement Element the container will be inserted after.
735 * @returns {Element} Autosuggest container element.
736 */
737 element = applyFilters('ep.Autosuggest.element', element, previousElement);
738
739 previousElement.insertAdjacentElement('afterend', element);
740 };
741
742 /**
743 * Prepare an input for Autosuggest.
744 *
745 * @param {Element} input Input to prepare.
746 * @returns {void}
747 */
748 const prepareInputForAutosuggest = (input) => {
749 /**
750 * Skip facet widget search fields and instant results.
751 */
752 const ignoredClasses = ['facet-search', 'ep-search-input'];
753 if (ignoredClasses.some((className) => input.classList.contains(className))) {
754 return;
755 }
756
757 /**
758 * Disable autocomplete.
759 */
760 input.setAttribute('autocomplete', 'off');
761
762 /**
763 * We know the markup of the Search block, so we don't need to add a
764 * wrapper.
765 */
766 if (input.classList.contains('wp-block-search__input')) {
767 input.form.classList.add('ep-autosuggest-container');
768 insertAutosuggestElement(input.parentElement);
769 } else {
770 wrapInAutosuggestContainer(input);
771 insertAutosuggestElement(input);
772 }
773
774 /**
775 * Dispatch an event announcing the input has moved.
776 */
777 const event = new CustomEvent('elasticpress.input.moved');
778
779 input.dispatchEvent(event);
780
781 /**
782 * Listen for any events:
783 *
784 * keyup
785 * send them for a query to the Elasticsearch server
786 * handle up and down keys to move between results
787 *
788 * blur
789 * hide the autosuggest box
790 */
791 input.addEventListener('keyup', handleKeyup);
792 input.addEventListener('blur', function () {
793 window.setTimeout(hideAutosuggestBox, 200);
794 });
795 };
796
797 /**
798 * Find inputs within an element and prepare them for Autosuggest.
799 *
800 * @param {Element} element Element to find inputs within.
801 * @returns {void}
802 */
803 const findAndPrepareInputsForAutosuggest = (element) => {
804 const inputs = element.querySelectorAll(selectors);
805
806 if (inputs) {
807 Array.from(inputs).forEach(prepareInputForAutosuggest);
808 }
809 };
810
811 /**
812 * Observe the document for new potential Autosuggest inputs, and add
813 * Autosuggest to any found inputs.
814 *
815 * @returns {void}
816 */
817 const observeDocumentForInputs = () => {
818 const target = document.body;
819 const config = {
820 subtree: true,
821 childList: true,
822 };
823
824 const observer = new MutationObserver((mutations, observer) => {
825 mutations.forEach((mutation) => {
826 Array.from(mutation.addedNodes).forEach((node) => {
827 if (node.nodeType !== Node.ELEMENT_NODE) {
828 return;
829 }
830
831 /**
832 * Adding autosuggest to an input moves it in the DOM,
833 * which would trigger our observer, so we need to
834 * stop observing until it's been prepared.
835 */
836 observer.disconnect();
837
838 /**
839 * If the node is an input, prepare it for Autosuggest if
840 * it matches the selectors, otherwise search the node for
841 * inputs.
842 */
843 if (node.tagName === 'INPUT') {
844 if (node.matches(selectors)) {
845 prepareInputForAutosuggest(node);
846 }
847 } else {
848 findAndPrepareInputsForAutosuggest(node);
849 }
850
851 /**
852 * Resume observing.
853 */
854 observer.observe(target, config);
855 });
856 });
857 });
858
859 observer.observe(target, config);
860 };
861
862 /**
863 * Add autosuggest to any inputs in the document.
864 */
865 findAndPrepareInputsForAutosuggest(document.body);
866
867 /**
868 * When the DOM is ready start observing for new inputs.
869 */
870 domReady(observeDocumentForInputs);
871 }
872