1 // This file is part of Moodle - http://moodle.org/
3 // Moodle is free software: you can redistribute it and/or modify
4 // it under the terms of the GNU General Public License as published by
5 // the Free Software Foundation, either version 3 of the License, or
6 // (at your option) any later version.
8 // Moodle is distributed in the hope that it will be useful,
9 // but WITHOUT ANY WARRANTY; without even the implied warranty of
10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 // GNU General Public License for more details.
13 // You should have received a copy of the GNU General Public License
14 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 * Autocomplete wrapper for select2 library.
19 * @module core/form-autocomplete
22 * @copyright 2015 Damyon Wiese <damyon@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 /* globals require: false */
27 define(['jquery', 'core/log', 'core/str', 'core/templates', 'core/notification'], function($, log, str, templates, notification) {
29 // Private functions and variables.
30 /** @var {Object} KEYS - List of keycode constants. */
41 * Make an item in the selection list "active".
43 * @method activateSelection
45 * @param {Number} index The index in the current (visible) list of selection.
46 * @param {Object} state State variables for this autocomplete element.
48 var activateSelection = function(index, state) {
49 // Find the elements in the DOM.
50 var selectionElement = $(document.getElementById(state.selectionId));
52 // Count the visible items.
53 var length = selectionElement.children('[aria-selected=true]').length;
54 // Limit the index to the upper/lower bounds of the list (wrap in both directions).
55 index = index % length;
59 // Find the specified element.
60 var element = $(selectionElement.children('[aria-selected=true]').get(index));
61 // Create an id we can assign to this element.
62 var itemId = state.selectionId + '-' + index;
64 // Deselect all the selections.
65 selectionElement.children().attr('data-active-selection', false).attr('id', '');
66 // Select only this suggestion and assign it the id.
67 element.attr('data-active-selection', true).attr('id', itemId);
68 // Tell the input field it has a new active descendant so the item is announced.
69 selectionElement.attr('aria-activedescendant', itemId);
73 * Update the element that shows the currently selected items.
75 * @method updateSelectionList
77 * @param {Object} options Original options for this autocomplete element.
78 * @param {Object} state State variables for this autocomplete element.
79 * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
81 var updateSelectionList = function(options, state, originalSelect) {
82 // Build up a valid context to re-render the template.
84 var newSelection = $(document.getElementById(state.selectionId));
85 var activeId = newSelection.attr('aria-activedescendant');
86 var activeValue = false;
89 activeValue = $(document.getElementById(activeId)).attr('data-value');
91 originalSelect.children('option').each(function(index, ele) {
92 if ($(ele).prop('selected')) {
93 items.push({label: $(ele).html(), value: $(ele).attr('value')});
96 var context = $.extend({items: items}, options, state);
98 // Render the template.
99 templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
100 // Add it to the page.
101 newSelection.empty().append($(newHTML).html());
103 if (activeValue !== false) {
104 // Reselect any previously selected item.
105 newSelection.children('[aria-selected=true]').each(function(index, ele) {
106 if ($(ele).attr('data-value') === activeValue) {
107 activateSelection(index, state);
111 }).fail(notification.exception);
115 * Notify of a change in the selection.
117 * @param {jQuery} originalSelect The jQuery object matching the hidden select list.
119 var notifyChange = function(originalSelect) {
120 if (typeof M.core_formchangechecker !== 'undefined') {
121 M.core_formchangechecker.set_form_changed();
123 originalSelect.change();
127 * Remove the given item from the list of selected things.
129 * @method deselectItem
131 * @param {Object} options Original options for this autocomplete element.
132 * @param {Object} state State variables for this autocomplete element.
133 * @param {Element} item The item to be deselected.
134 * @param {Element} originalSelect The original select list.
136 var deselectItem = function(options, state, item, originalSelect) {
137 var selectedItemValue = $(item).attr('data-value');
139 // We can only deselect items if this is a multi-select field.
140 if (options.multiple) {
141 // Look for a match, and toggle the selected property if there is a match.
142 originalSelect.children('option').each(function(index, ele) {
143 if ($(ele).attr('value') == selectedItemValue) {
144 $(ele).prop('selected', false);
145 // We remove newly created custom tags from the suggestions list when they are deselected.
146 if ($(ele).attr('data-iscustom')) {
152 // Rerender the selection list.
153 updateSelectionList(options, state, originalSelect);
154 // Notifiy that the selection changed.
155 notifyChange(originalSelect);
159 * Make an item in the suggestions "active" (about to be selected).
161 * @method activateItem
163 * @param {Number} index The index in the current (visible) list of suggestions.
164 * @param {Object} state State variables for this instance of autocomplete.
166 var activateItem = function(index, state) {
167 // Find the elements in the DOM.
168 var inputElement = $(document.getElementById(state.inputId));
169 var suggestionsElement = $(document.getElementById(state.suggestionsId));
171 // Count the visible items.
172 var length = suggestionsElement.children('[aria-hidden=false]').length;
173 // Limit the index to the upper/lower bounds of the list (wrap in both directions).
174 index = index % length;
178 // Find the specified element.
179 var element = $(suggestionsElement.children('[aria-hidden=false]').get(index));
180 // Find the index of this item in the full list of suggestions (including hidden).
181 var globalIndex = $(suggestionsElement.children('[role=option]')).index(element);
182 // Create an id we can assign to this element.
183 var itemId = state.suggestionsId + '-' + globalIndex;
185 // Deselect all the suggestions.
186 suggestionsElement.children().attr('aria-selected', false).attr('id', '');
187 // Select only this suggestion and assign it the id.
188 element.attr('aria-selected', true).attr('id', itemId);
189 // Tell the input field it has a new active descendant so the item is announced.
190 inputElement.attr('aria-activedescendant', itemId);
192 // Scroll it into view.
193 var scrollPos = element.offset().top
194 - suggestionsElement.offset().top
195 + suggestionsElement.scrollTop()
196 - (suggestionsElement.height() / 2);
197 suggestionsElement.animate({
203 * Find the index of the current active suggestion, and activate the next one.
205 * @method activateNextItem
207 * @param {Object} state State variable for this auto complete element.
209 var activateNextItem = function(state) {
210 // Find the list of suggestions.
211 var suggestionsElement = $(document.getElementById(state.suggestionsId));
212 // Find the active one.
213 var element = suggestionsElement.children('[aria-selected=true]');
215 var current = suggestionsElement.children('[aria-hidden=false]').index(element);
216 // Activate the next one.
217 activateItem(current + 1, state);
221 * Find the index of the current active selection, and activate the previous one.
223 * @method activatePreviousSelection
225 * @param {Object} state State variables for this instance of autocomplete.
227 var activatePreviousSelection = function(state) {
228 // Find the list of selections.
229 var selectionsElement = $(document.getElementById(state.selectionId));
230 // Find the active one.
231 var element = selectionsElement.children('[data-active-selection=true]');
233 activateSelection(0, state);
237 var current = selectionsElement.children('[aria-selected=true]').index(element);
238 // Activate the next one.
239 activateSelection(current - 1, state);
242 * Find the index of the current active selection, and activate the next one.
244 * @method activateNextSelection
246 * @param {Object} state State variables for this instance of autocomplete.
248 var activateNextSelection = function(state) {
249 // Find the list of selections.
250 var selectionsElement = $(document.getElementById(state.selectionId));
251 // Find the active one.
252 var element = selectionsElement.children('[data-active-selection=true]');
254 activateSelection(0, state);
258 var current = selectionsElement.children('[aria-selected=true]').index(element);
259 // Activate the next one.
260 activateSelection(current + 1, state);
264 * Find the index of the current active suggestion, and activate the previous one.
266 * @method activatePreviousItem
268 * @param {Object} state State variables for this autocomplete element.
270 var activatePreviousItem = function(state) {
271 // Find the list of suggestions.
272 var suggestionsElement = $(document.getElementById(state.suggestionsId));
273 // Find the active one.
274 var element = suggestionsElement.children('[aria-selected=true]');
276 var current = suggestionsElement.children('[aria-hidden=false]').index(element);
277 // Activate the next one.
278 activateItem(current - 1, state);
282 * Close the list of suggestions.
284 * @method closeSuggestions
286 * @param {Object} state State variables for this autocomplete element.
288 var closeSuggestions = function(state) {
289 // Find the elements in the DOM.
290 var inputElement = $(document.getElementById(state.inputId));
291 var suggestionsElement = $(document.getElementById(state.suggestionsId));
293 // Announce the list of suggestions was closed, and read the current list of selections.
294 inputElement.attr('aria-expanded', false).attr('aria-activedescendant', state.selectionId);
295 // Hide the suggestions list (from screen readers too).
296 suggestionsElement.hide().attr('aria-hidden', true);
300 * Rebuild the list of suggestions based on the current values in the select list, and the query.
302 * @method updateSuggestions
304 * @param {Object} options The original options for this autocomplete.
305 * @param {Object} state The state variables for this autocomplete.
306 * @param {String} query The current text for the search string.
307 * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
309 var updateSuggestions = function(options, state, query, originalSelect) {
310 // Find the elements in the DOM.
311 var inputElement = $(document.getElementById(state.inputId));
312 var suggestionsElement = $(document.getElementById(state.suggestionsId));
314 // Used to track if we found any visible suggestions.
315 var matchingElements = false;
316 // Options is used by the context when rendering the suggestions from a template.
317 var suggestions = [];
318 originalSelect.children('option').each(function(index, option) {
319 if ($(option).prop('selected') !== true) {
320 suggestions[suggestions.length] = {label: option.innerHTML, value: $(option).attr('value')};
324 // Re-render the list of suggestions.
325 var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
326 var context = $.extend({options: suggestions}, options, state);
328 'core/form_autocomplete_suggestions',
330 ).done(function(newHTML) {
331 // We have the new template, insert it in the page.
332 suggestionsElement.replaceWith(newHTML);
333 // Get the element again.
334 suggestionsElement = $(document.getElementById(state.suggestionsId));
335 // Show it if it is hidden.
336 suggestionsElement.show().attr('aria-hidden', false);
337 // For each option in the list, hide it if it doesn't match the query.
338 suggestionsElement.children().each(function(index, node) {
340 if ((options.caseSensitive && node.text().indexOf(searchquery) > -1) ||
341 (!options.caseSensitive && node.text().toLocaleLowerCase().indexOf(searchquery) > -1)) {
342 node.show().attr('aria-hidden', false);
343 matchingElements = true;
345 node.hide().attr('aria-hidden', true);
348 // If we found any matches, show the list.
349 inputElement.attr('aria-expanded', true);
350 if (matchingElements) {
351 // We only activate the first item in the list if tags is false,
352 // because otherwise "Enter" would select the first item, instead of
353 // creating a new tag.
355 activateItem(0, state);
358 // Nothing matches. Tell them that.
359 str.get_string('nosuggestions', 'form').done(function(nosuggestionsstr) {
360 suggestionsElement.html(nosuggestionsstr);
363 }).fail(notification.exception);
368 * Create a new item for the list (a tag).
372 * @param {Object} options The original options for the autocomplete.
373 * @param {Object} state State variables for the autocomplete.
374 * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
376 var createItem = function(options, state, originalSelect) {
377 // Find the element in the DOM.
378 var inputElement = $(document.getElementById(state.inputId));
379 // Get the current text in the input field.
380 var query = inputElement.val();
381 var tags = query.split(',');
384 $.each(tags, function(tagindex, tag) {
385 // If we can only select one at a time, deselect any current value.
388 if (!options.multiple) {
389 originalSelect.children('option').prop('selected', false);
391 // Look for an existing option in the select list that matches this new tag.
392 originalSelect.children('option').each(function(index, ele) {
393 if ($(ele).attr('value') == tag) {
395 $(ele).prop('selected', true);
398 // Only create the item if it's new.
400 var option = $('<option>');
402 option.attr('value', tag);
403 originalSelect.append(option);
404 option.prop('selected', true);
405 // We mark newly created custom options as we handle them differently if they are "deselected".
406 option.attr('data-iscustom', true);
411 updateSelectionList(options, state, originalSelect);
412 // Notifiy that the selection changed.
413 notifyChange(originalSelect);
414 // Clear the input field.
415 inputElement.val('');
416 // Close the suggestions list.
417 closeSuggestions(state);
421 * Select the currently active item from the suggestions list.
423 * @method selectCurrentItem
425 * @param {Object} options The original options for the autocomplete.
426 * @param {Object} state State variables for the autocomplete.
427 * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
429 var selectCurrentItem = function(options, state, originalSelect) {
430 // Find the elements in the page.
431 var inputElement = $(document.getElementById(state.inputId));
432 var suggestionsElement = $(document.getElementById(state.suggestionsId));
433 // Here loop through suggestions and set val to join of all selected items.
435 var selectedItemValue = suggestionsElement.children('[aria-selected=true]').attr('data-value');
436 // The select will either be a single or multi select, so the following will either
437 // select one or more items correctly.
438 // Take care to use 'prop' and not 'attr' for selected properties.
439 // If only one can be selected at a time, start by deselecting everything.
440 if (!options.multiple) {
441 originalSelect.children('option').prop('selected', false);
443 // Look for a match, and toggle the selected property if there is a match.
444 originalSelect.children('option').each(function(index, ele) {
445 if ($(ele).attr('value') == selectedItemValue) {
446 $(ele).prop('selected', true);
449 // Rerender the selection list.
450 updateSelectionList(options, state, originalSelect);
451 // Notifiy that the selection changed.
452 notifyChange(originalSelect);
454 if (options.closeSuggestionsOnSelect) {
455 // Clear the input element.
456 inputElement.val('');
457 // Close the list of suggestions.
458 closeSuggestions(state);
460 // Focus on the input element so the suggestions does not auto-close.
461 inputElement.focus();
462 // Remove the last selected item from the suggestions list.
463 updateSuggestions(options, state, inputElement.val(), originalSelect);
468 * Fetch a new list of options via ajax.
472 * @param {Event} e The event that triggered this update.
473 * @param {Object} options The original options for the autocomplete.
474 * @param {Object} state The state variables for the autocomplete.
475 * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
476 * @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
478 var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
479 // Get the query to pass to the ajax function.
480 var query = $(e.currentTarget).val();
481 // Call the transport function to do the ajax (name taken from Select2).
482 ajaxHandler.transport(options.selector, query, function(results) {
483 // We got a result - pass it through the translator before using it.
484 var processedResults = ajaxHandler.processResults(options.selector, results);
485 var existingValues = [];
487 // Now destroy all options that are not currently selected.
488 originalSelect.children('option').each(function(optionIndex, option) {
490 if (!option.prop('selected')) {
493 existingValues.push(String(option.attr('value')));
497 if (!options.multiple && originalSelect.children('option').length === 0) {
498 // If this is a single select - and there are no current options
499 // the first option added will be selected by the browser. This causes a bug!
500 // We need to insert an empty option so that none of the real options are selected.
501 var option = $('<option>');
502 originalSelect.append(option);
504 // And add all the new ones returned from ajax.
505 $.each(processedResults, function(resultIndex, result) {
506 if (existingValues.indexOf(String(result.value)) === -1) {
507 var option = $('<option>');
508 option.append(result.label);
509 option.attr('value', result.value);
510 originalSelect.append(option);
513 // Update the list of suggestions now from the new values in the select list.
514 updateSuggestions(options, state, '', originalSelect);
515 }, notification.exception);
519 * Add all the event listeners required for keyboard nav, blur clicks etc.
521 * @method addNavigation
523 * @param {Object} options The options used to create this autocomplete element.
524 * @param {Object} state State variables for this autocomplete element.
525 * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
527 var addNavigation = function(options, state, originalSelect) {
528 // Start with the input element.
529 var inputElement = $(document.getElementById(state.inputId));
530 // Add keyboard nav with keydown.
531 inputElement.on('keydown', function(e) {
534 // If the suggestion list is open, move to the next item.
535 if (!options.showSuggestions) {
536 // Do not consume this event.
538 } else if (inputElement.attr('aria-expanded') === "true") {
539 activateNextItem(state);
541 // Handle ajax population of suggestions.
542 if (!inputElement.val() && options.ajax) {
543 require([options.ajax], function(ajaxHandler) {
544 updateAjax(e, options, state, originalSelect, ajaxHandler);
547 // Open the suggestions list.
548 updateSuggestions(options, state, inputElement.val(), originalSelect);
551 // We handled this event, so prevent it.
555 // Choose the previous active item.
556 activatePreviousItem(state);
557 // We handled this event, so prevent it.
561 var suggestionsElement = $(document.getElementById(state.suggestionsId));
562 if ((inputElement.attr('aria-expanded') === "true") &&
563 (suggestionsElement.children('[aria-selected=true]').length > 0)) {
564 // If the suggestion list has an active item, select it.
565 selectCurrentItem(options, state, originalSelect);
566 } else if (options.tags) {
567 // If tags are enabled, create a tag.
568 createItem(options, state, originalSelect);
570 // We handled this event, so prevent it.
574 if (inputElement.attr('aria-expanded') === "true") {
575 // If the suggestion list is open, close it.
576 closeSuggestions(state);
578 // We handled this event, so prevent it.
584 // Support multi lingual COMMA keycode (44).
585 inputElement.on('keypress', function(e) {
586 if (e.keyCode === KEYS.COMMA) {
588 // If we are allowing tags, comma should create a tag (or enter).
589 createItem(options, state, originalSelect);
591 // We handled this event, so prevent it.
597 // Handler used to force set the value from behat.
598 inputElement.on('behat:set-value', function() {
599 var suggestionsElement = $(document.getElementById(state.suggestionsId));
600 if ((inputElement.attr('aria-expanded') === "true") &&
601 (suggestionsElement.children('[aria-selected=true]').length > 0)) {
602 // If the suggestion list has an active item, select it.
603 selectCurrentItem(options, state, originalSelect);
604 } else if (options.tags) {
605 // If tags are enabled, create a tag.
606 createItem(options, state, originalSelect);
609 inputElement.on('blur', function() {
610 window.setTimeout(function() {
611 // Get the current element with focus.
612 var focusElement = $(document.activeElement);
613 // Only close the menu if the input hasn't regained focus.
614 if (focusElement.attr('id') != inputElement.attr('id')) {
616 createItem(options, state, originalSelect);
618 closeSuggestions(state);
622 if (options.showSuggestions) {
623 var arrowElement = $(document.getElementById(state.downArrowId));
624 arrowElement.on('click', function(e) {
625 // Prevent the close timer, or we will open, then close the suggestions.
626 inputElement.focus();
627 // Handle ajax population of suggestions.
628 if (!inputElement.val() && options.ajax) {
629 require([options.ajax], function(ajaxHandler) {
630 updateAjax(e, options, state, originalSelect, ajaxHandler);
633 // Else - open the suggestions list.
634 updateSuggestions(options, state, inputElement.val(), originalSelect);
639 var suggestionsElement = $(document.getElementById(state.suggestionsId));
640 suggestionsElement.parent().on('click', '[role=option]', function(e) {
641 // Handle clicks on suggestions.
642 var element = $(e.currentTarget).closest('[role=option]');
643 var suggestionsElement = $(document.getElementById(state.suggestionsId));
644 // Find the index of the clicked on suggestion.
645 var current = suggestionsElement.children('[aria-hidden=false]').index(element);
647 activateItem(current, state);
649 selectCurrentItem(options, state, originalSelect);
651 var selectionElement = $(document.getElementById(state.selectionId));
652 // Handle clicks on the selected items (will unselect an item).
653 selectionElement.on('click', '[role=listitem]', function(e) {
654 // Get the item that was clicked.
655 var item = $(e.currentTarget);
656 // Remove it from the selection.
657 deselectItem(options, state, item, originalSelect);
659 // Keyboard navigation for the selection list.
660 selectionElement.on('keydown', function(e) {
663 // Choose the next selection item.
664 activateNextSelection(state);
665 // We handled this event, so prevent it.
669 // Choose the previous selection item.
670 activatePreviousSelection(state);
671 // We handled this event, so prevent it.
676 // Get the item that is currently selected.
677 var selectedItem = $(document.getElementById(state.selectionId)).children('[data-active-selection=true]');
679 // Unselect this item.
680 deselectItem(options, state, selectedItem, originalSelect);
681 // We handled this event, so prevent it.
688 // Whenever the input field changes, update the suggestion list.
689 if (options.showSuggestions) {
690 inputElement.on('input', function(e) {
691 var query = $(e.currentTarget).val();
692 var last = $(e.currentTarget).data('last-value');
693 // IE11 fires many more input events than required - even when the value has not changed.
694 // We need to only do this for real value changed events or the suggestions will be
695 // unclickable on IE11 (because they will be rebuilt before the click event fires).
696 // Note - because of this we cannot close the list when the query is empty or it will break
698 if (last !== query) {
699 updateSuggestions(options, state, query, originalSelect);
701 $(e.currentTarget).data('last-value', query);
706 return /** @alias module:core/form-autocomplete */ {
707 // Public variables and functions.
709 * Turn a boring select box into an auto-complete beast.
712 * @param {string} selector The selector that identifies the select box.
713 * @param {boolean} tags Whether to allow support for tags (can define new entries).
714 * @param {string} ajax Name of an AMD module to handle ajax requests. If specified, the AMD
715 * module must expose 2 functions "transport" and "processResults".
716 * These are modeled on Select2 see: https://select2.github.io/options.html#ajax
717 * @param {String} placeholder - The text to display before a selection is made.
718 * @param {Boolean} caseSensitive - If search has to be made case sensitive.
719 * @param {Boolean} showSuggestions - If suggestions should be shown
720 * @param {String} noSelectionString - Text to display when there is no selection
721 * @param {Boolean} closeSuggestionsOnSelect - Whether to close the suggestions immediately after making a selection.
724 enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString,
725 closeSuggestionsOnSelect) {
726 // Set some default values.
731 placeholder: placeholder,
732 caseSensitive: false,
733 showSuggestions: true,
734 noSelectionString: noSelectionString
736 if (typeof tags !== "undefined") {
739 if (typeof ajax !== "undefined") {
742 if (typeof caseSensitive !== "undefined") {
743 options.caseSensitive = caseSensitive;
745 if (typeof showSuggestions !== "undefined") {
746 options.showSuggestions = showSuggestions;
748 if (typeof noSelectionString === "undefined") {
749 str.get_string('noselection', 'form').done(function(result) {
750 options.noSelectionString = result;
751 }).fail(notification.exception);
754 // Look for the select element.
755 var originalSelect = $(selector);
756 if (!originalSelect) {
757 log.debug('Selector not found: ' + selector);
761 // Hide the original select.
762 originalSelect.hide().attr('aria-hidden', true);
764 // Find or generate some ids.
766 selectId: originalSelect.attr('id'),
767 inputId: 'form_autocomplete_input-' + $.now(),
768 suggestionsId: 'form_autocomplete_suggestions-' + $.now(),
769 selectionId: 'form_autocomplete_selection-' + $.now(),
770 downArrowId: 'form_autocomplete_downarrow-' + $.now()
772 options.multiple = originalSelect.attr('multiple');
774 if (typeof closeSuggestionsOnSelect !== "undefined") {
775 options.closeSuggestionsOnSelect = closeSuggestionsOnSelect;
777 // If not specified, this will close suggestions by default for single-select elements only.
778 options.closeSuggestionsOnSelect = !options.multiple;
781 var originalLabel = $('[for=' + state.selectId + ']');
782 // Create the new markup and insert it after the select.
783 var suggestions = [];
784 originalSelect.children('option').each(function(index, option) {
785 suggestions[index] = {label: option.innerHTML, value: $(option).attr('value')};
788 // Render all the parts of our UI.
789 var context = $.extend({}, options, state);
790 context.options = suggestions;
793 var renderInput = templates.render('core/form_autocomplete_input', context);
794 var renderDatalist = templates.render('core/form_autocomplete_suggestions', context);
795 var renderSelection = templates.render('core/form_autocomplete_selection', context);
797 return $.when(renderInput, renderDatalist, renderSelection).then(function(input, suggestions, selection) {
798 // Add our new UI elements to the page.
799 originalSelect.after(suggestions);
800 originalSelect.after(input);
801 originalSelect.after(selection);
802 // Update the form label to point to the text input.
803 originalLabel.attr('for', state.inputId);
804 // Add the event handlers.
805 addNavigation(options, state, originalSelect);
807 var inputElement = $(document.getElementById(state.inputId));
808 var suggestionsElement = $(document.getElementById(state.suggestionsId));
809 // Hide the suggestions by default.
810 suggestionsElement.hide().attr('aria-hidden', true);
812 // If this field uses ajax, set it up.
814 require([options.ajax], function(ajaxHandler) {
815 var throttleTimeout = null;
816 var handler = function(e) {
817 updateAjax(e, options, state, originalSelect, ajaxHandler);
820 // For input events, we do not want to trigger many, many updates.
821 var throttledHandler = function(e) {
822 if (throttleTimeout !== null) {
823 window.clearTimeout(throttleTimeout);
824 throttleTimeout = null;
826 throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
828 // Trigger an ajax update after the text field value changes.
829 inputElement.on("input", throttledHandler);
832 // Show the current values in the selection list.
833 updateSelectionList(options, state, originalSelect);