MDL-51914 qtype_ddmarker: fix checking points on the boundary
[moodle.git] / lib / amd / src / form-autocomplete.js
1 // This file is part of Moodle - http://moodle.org/
2 //
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.
7 //
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.
12 //
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/>.
16 /**
17  * Autocomplete wrapper for select2 library.
18  *
19  * @module     core/form-autocomplete
20  * @class      autocomplete
21  * @package    core
22  * @copyright  2015 Damyon Wiese <damyon@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  * @since      3.0
25  */
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. */
31     var KEYS = {
32         DOWN: 40,
33         ENTER: 13,
34         SPACE: 32,
35         ESCAPE: 27,
36         COMMA: 188,
37         UP: 38
38     };
40     /** @var {Number} closeSuggestionsTimer - integer used to cancel window.setTimeout. */
41     var closeSuggestionsTimer = null;
43     /**
44      * Make an item in the selection list "active".
45      *
46      * @method activateSelection
47      * @private
48      * @param {Number} index The index in the current (visible) list of selection.
49      * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
50      */
51     var activateSelection = function(index, selectionId) {
52         // Find the elements in the DOM.
53         var selectionElement = $(document.getElementById(selectionId));
55         // Count the visible items.
56         var length = selectionElement.children('[aria-selected=true]').length;
57         // Limit the index to the upper/lower bounds of the list (wrap in both directions).
58         index = index % length;
59         while (index < 0) {
60             index += length;
61         }
62         // Find the specified element.
63         var element = $(selectionElement.children('[aria-selected=true]').get(index));
64         // Create an id we can assign to this element.
65         var itemId = selectionId + '-' + index;
67         // Deselect all the selections.
68         selectionElement.children().attr('data-active-selection', false).attr('id', '');
69         // Select only this suggestion and assign it the id.
70         element.attr('data-active-selection', true).attr('id', itemId);
71         // Tell the input field it has a new active descendant so the item is announced.
72         selectionElement.attr('aria-activedescendant', itemId);
73     };
75     /**
76      * Remove the given item from the list of selected things.
77      *
78      * @method deselectItem
79      * @private
80      * @param {String} inputId The id of the input element for this instance of the autocomplete.
81      * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
82      * @param {Element} The item to be deselected.
83      * @param {Element} originalSelect The original select list.
84      * @param {Boolean} multiple Is this a multi select.
85      */
86     var deselectItem = function(inputId, selectionId, item, originalSelect, multiple) {
87         var selectedItemValue = $(item).attr('data-value');
89         // We can only deselect items if this is a multi-select field.
90         if (multiple) {
91             // Look for a match, and toggle the selected property if there is a match.
92             originalSelect.children('option').each(function(index, ele) {
93                 if ($(ele).attr('value') == selectedItemValue) {
94                     $(ele).prop('selected', false);
95                 }
96             });
97         }
98         // Rerender the selection list.
99         updateSelectionList(selectionId, inputId, originalSelect, multiple);
100     };
102     /**
103      * Make an item in the suggestions "active" (about to be selected).
104      *
105      * @method activateItem
106      * @private
107      * @param {Number} index The index in the current (visible) list of suggestions.
108      * @param {String} inputId The id of the input element for this instance of the autocomplete.
109      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
110      */
111     var activateItem = function(index, inputId, suggestionsId) {
112         // Find the elements in the DOM.
113         var inputElement = $(document.getElementById(inputId));
114         var suggestionsElement = $(document.getElementById(suggestionsId));
116         // Count the visible items.
117         var length = suggestionsElement.children('[aria-hidden=false]').length;
118         // Limit the index to the upper/lower bounds of the list (wrap in both directions).
119         index = index % length;
120         while (index < 0) {
121             index += length;
122         }
123         // Find the specified element.
124         var element = $(suggestionsElement.children('[aria-hidden=false]').get(index));
125         // Find the index of this item in the full list of suggestions (including hidden).
126         var globalIndex = $(suggestionsElement.children('[role=option]')).index(element);
127         // Create an id we can assign to this element.
128         var itemId = suggestionsId + '-' + globalIndex;
130         // Deselect all the suggestions.
131         suggestionsElement.children().attr('aria-selected', false).attr('id', '');
132         // Select only this suggestion and assign it the id.
133         element.attr('aria-selected', true).attr('id', itemId);
134         // Tell the input field it has a new active descendant so the item is announced.
135         inputElement.attr('aria-activedescendant', itemId);
136     };
138     /**
139      * Find the index of the current active suggestion, and activate the next one.
140      *
141      * @method activateNextItem
142      * @private
143      * @param {String} inputId The id of the input element for this instance of the autocomplete.
144      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
145      */
146     var activateNextItem = function(inputId, suggestionsId) {
147         // Find the list of suggestions.
148         var suggestionsElement = $(document.getElementById(suggestionsId));
149         // Find the active one.
150         var element = suggestionsElement.children('[aria-selected=true]');
151         // Find it's index.
152         var current = suggestionsElement.children('[aria-hidden=false]').index(element);
153         // Activate the next one.
154         activateItem(current+1, inputId, suggestionsId);
155     };
157     /**
158      * Find the index of the current active selection, and activate the previous one.
159      *
160      * @method activatePreviousSelection
161      * @private
162      * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
163      */
164     var activatePreviousSelection = function(selectionId) {
165         // Find the list of selections.
166         var selectionsElement = $(document.getElementById(selectionId));
167         // Find the active one.
168         var element = selectionsElement.children('[data-active-selection=true]');
169         if (!element) {
170             activateSelection(0, selectionId);
171             return;
172         }
173         // Find it's index.
174         var current = selectionsElement.children('[aria-selected=true]').index(element);
175         // Activate the next one.
176         activateSelection(current-1, selectionId);
177     };
178     /**
179      * Find the index of the current active selection, and activate the next one.
180      *
181      * @method activateNextSelection
182      * @private
183      * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
184      */
185     var activateNextSelection = function(selectionId) {
186         // Find the list of selections.
187         var selectionsElement = $(document.getElementById(selectionId));
188         // Find the active one.
189         var element = selectionsElement.children('[data-active-selection=true]');
190         if (!element) {
191             activateSelection(0, selectionId);
192             return;
193         }
194         // Find it's index.
195         var current = selectionsElement.children('[aria-selected=true]').index(element);
196         // Activate the next one.
197         activateSelection(current+1, selectionId);
198     };
200     /**
201      * Find the index of the current active suggestion, and activate the previous one.
202      *
203      * @method activatePreviousItem
204      * @private
205      * @param {String} inputId The id of the input element for this instance of the autocomplete.
206      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
207      */
208     var activatePreviousItem = function(inputId, suggestionsId) {
209         // Find the list of suggestions.
210         var suggestionsElement = $(document.getElementById(suggestionsId));
211         // Find the active one.
212         var element = suggestionsElement.children('[aria-selected=true]');
213         // Find it's index.
214         var current = suggestionsElement.children('[aria-hidden=false]').index(element);
215         // Activate the next one.
216         activateItem(current-1, inputId, suggestionsId);
217     };
219     /**
220      * Close the list of suggestions.
221      *
222      * @method closeSuggestions
223      * @private
224      * @param {String} inputId The id of the input element for this instance of the autocomplete.
225      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
226      */
227     var closeSuggestions = function(inputId, suggestionsId, selectionId) {
228         // Find the elements in the DOM.
229         var inputElement = $(document.getElementById(inputId));
230         var suggestionsElement = $(document.getElementById(suggestionsId));
232         // Announce the list of suggestions was closed, and read the current list of selections.
233         inputElement.attr('aria-expanded', false).attr('aria-activedescendant', selectionId);
234         // Hide the suggestions list (from screen readers too).
235         suggestionsElement.hide().attr('aria-hidden', true);
236     };
238     /**
239      * Rebuild the list of suggestions based on the current values in the select list, and the query.
240      *
241      * @method updateSuggestions
242      * @private
243      * @param {String} query The current query typed in the input field.
244      * @param {String} inputId The id of the input element for this instance of the autocomplete.
245      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
246      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
247      * @param {Boolean} multiple Are multiple items allowed to be selected?
248      * @param {Boolean} tags Are we allowed to create new items on the fly?
249      */
250     var updateSuggestions = function(query, inputId, suggestionsId, originalSelect, multiple, tags) {
251         // Find the elements in the DOM.
252         var inputElement = $(document.getElementById(inputId));
253         var suggestionsElement = $(document.getElementById(suggestionsId));
255         // Used to track if we found any visible suggestions.
256         var matchingElements = false;
257         // Options is used by the context when rendering the suggestions from a template.
258         var options = [];
259         originalSelect.children('option').each(function(index, option) {
260             if ($(option).prop('selected') !== true) {
261                 options[options.length] = { label: option.innerHTML, value: $(option).attr('value') };
262             }
263         });
265         // Re-render the list of suggestions.
266         templates.render(
267             'core/form_autocomplete_suggestions',
268             { inputId: inputId, suggestionsId: suggestionsId, options: options, multiple: multiple}
269         ).done(function(newHTML) {
270             // We have the new template, insert it in the page.
271             suggestionsElement.replaceWith(newHTML);
272             // Get the element again.
273             suggestionsElement = $(document.getElementById(suggestionsId));
274             // Show it if it is hidden.
275             suggestionsElement.show().attr('aria-hidden', false);
276             // For each option in the list, hide it if it doesn't match the query.
277             suggestionsElement.children().each(function(index, node) {
278                 node = $(node);
279                 if (node.text().indexOf(query) > -1) {
280                     node.show().attr('aria-hidden', false);
281                     matchingElements = true;
282                 } else {
283                     node.hide().attr('aria-hidden', true);
284                 }
285             });
286             // If we found any matches, show the list.
287             if (matchingElements) {
288                 inputElement.attr('aria-expanded', true);
289                 // We only activate the first item in the list if tags is false,
290                 // because otherwise "Enter" would select the first item, instead of
291                 // creating a new tag.
292                 if (!tags) {
293                     activateItem(0, inputId, suggestionsId);
294                 }
295             } else {
296                 // Abort - nothing matches. Hide the suggestions properly.
297                 suggestionsElement.hide();
298                 suggestionsElement.attr('aria-hidden', true);
299                 inputElement.attr('aria-expanded', false);
300             }
301         }).fail(notification.exception);
303     };
305     /**
306      * Create a new item for the list (a tag).
307      *
308      * @method createItem
309      * @private
310      * @param {String} inputId The id of the input element for this instance of the autocomplete.
311      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
312      * @param {Boolean} multiple Are multiple items allowed to be selected?
313      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
314      */
315     var createItem = function(inputId, suggestionsId, selectionId, multiple, originalSelect) {
316         // Find the element in the DOM.
317         var inputElement = $(document.getElementById(inputId));
318         // Get the current text in the input field.
319         var query = inputElement.val();
320         var tags = query.split(',');
321         var found = false;
323         $.each(tags, function(tagindex, tag) {
324             // If we can only select one at a time, deselect any current value.
325             tag = tag.trim();
326             if (tag !== '') {
327                 if (!multiple) {
328                     originalSelect.children('option').prop('selected', false);
329                 }
330                 // Look for an existing option in the select list that matches this new tag.
331                 originalSelect.children('option').each(function(index, ele) {
332                     if ($(ele).attr('value') == tag) {
333                         found = true;
334                         $(ele).prop('selected', true);
335                     }
336                 });
337                 // Only create the item if it's new.
338                 if (!found) {
339                     var option = $('<option>');
340                     option.append(tag);
341                     option.attr('value', tag);
342                     originalSelect.append(option);
343                     option.prop('selected', true);
344                 }
345             }
346         });
347         // Get the selection element.
348         var newSelection = $(document.getElementById(selectionId));
349         // Build up a valid context to re-render the selection.
350         var items = [];
351         originalSelect.children('option').each(function(index, ele) {
352             if ($(ele).prop('selected')) {
353                 items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
354             }
355         });
356         var context = {
357             selectionId: selectionId,
358             items: items,
359             multiple: multiple
360         };
361         // Re-render the selection.
362         templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
363             // Update the page.
364             newSelection.empty().append($(newHTML).html());
365         }).fail(notification.exception);
366         // Clear the input field.
367         inputElement.val('');
368         // Close the suggestions list.
369         closeSuggestions(inputId, suggestionsId, selectionId);
370         // Trigger a change event so that the mforms javascript can check for required fields etc.
371         originalSelect.change();
372     };
374     /**
375      * Update the element that shows the currently selected items.
376      *
377      * @method updateSelectionList
378      * @private
379      * @param {String} selectionId The id of the selections element for this instance of the autocomplete.
380      * @param {String} inputId The id of the input element for this instance of the autocomplete.
381      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
382      * @param {Boolean} multiple Does this element support multiple selections.
383      */
384     var updateSelectionList = function(selectionId, inputId, originalSelect, multiple) {
385         // Build up a valid context to re-render the template.
386         var items = [];
387         var newSelection = $(document.getElementById(selectionId));
388         originalSelect.children('option').each(function(index, ele) {
389             if ($(ele).prop('selected')) {
390                 items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
391             }
392         });
393         var context = {
394             selectionId: selectionId,
395             items: items,
396             multiple: multiple
397         };
398         // Render the template.
399         templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
400             // Add it to the page.
401             newSelection.empty().append($(newHTML).html());
402         }).fail(notification.exception);
403         // Because this function get's called after changing the selection, this is a good place
404         // to trigger a change notification.
405         originalSelect.change();
406     };
408     /**
409      * Select the currently active item from the suggestions list.
410      *
411      * @method selectCurrentItem
412      * @private
413      * @param {String} inputId The id of the input element for this instance of the autocomplete.
414      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
415      * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
416      * @param {Boolean} multiple Are multiple items allowed to be selected?
417      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
418      */
419     var selectCurrentItem = function(inputId, suggestionsId, selectionId, multiple, originalSelect) {
420         // Find the elements in the page.
421         var inputElement = $(document.getElementById(inputId));
422         var suggestionsElement = $(document.getElementById(suggestionsId));
423         // Here loop through suggestions and set val to join of all selected items.
425         var selectedItemValue = suggestionsElement.children('[aria-selected=true]').attr('data-value');
426         // The select will either be a single or multi select, so the following will either
427         // select one or more items correctly.
428         // Take care to use 'prop' and not 'attr' for selected properties.
429         // If only one can be selected at a time, start by deselecting everything.
430         if (!multiple) {
431             originalSelect.children('option').prop('selected', false);
432         }
433         // Look for a match, and toggle the selected property if there is a match.
434         originalSelect.children('option').each(function(index, ele) {
435             if ($(ele).attr('value') == selectedItemValue) {
436                 $(ele).prop('selected', true);
437             }
438         });
439         // Rerender the selection list.
440         updateSelectionList(selectionId, inputId, originalSelect, multiple);
441         // Clear the input element.
442         inputElement.val('');
443         // Close the list of suggestions.
444         closeSuggestions(inputId, suggestionsId, selectionId);
445     };
447     /**
448      * Fetch a new list of options via ajax.
449      *
450      * @method updateAjax
451      * @private
452      * @param {Event} e The event that triggered this update.
453      * @param {String} selector The selector pointing to the original select.
454      * @param {String} inputId The id of the input element for this instance of the autocomplete.
455      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
456      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
457      * @param {Boolean} multiple Are multiple items allowed to be selected?
458      * @param {Boolean} tags Are we allowed to create new items on the fly?
459      * @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
460      */
461     var updateAjax = function(e, selector, inputId, suggestionsId, originalSelect, multiple, tags, ajaxHandler) {
462         // Get the query to pass to the ajax function.
463         var query = $(e.currentTarget).val();
464         // Call the transport function to do the ajax (name taken from Select2).
465         ajaxHandler.transport(selector, query, function(results) {
466             // We got a result - pass it through the translator before using it.
467             var processedResults = ajaxHandler.processResults(selector, results);
468             var existingValues = [];
470             // Now destroy all options that are not currently selected.
471             originalSelect.children('option').each(function(optionIndex, option) {
472                 option = $(option);
473                 if (!option.prop('selected')) {
474                     option.remove();
475                 } else {
476                     existingValues.push(option.attr('value'));
477                 }
478             });
479             // And add all the new ones returned from ajax.
480             $.each(processedResults, function(resultIndex, result) {
481                 if (existingValues.indexOf(result.value) === -1) {
482                     var option = $('<option>');
483                     option.append(result.label);
484                     option.attr('value', result.value);
485                     originalSelect.append(option);
486                 }
487             });
488             // Update the list of suggestions now from the new values in the select list.
489             updateSuggestions('', inputId, suggestionsId, originalSelect, multiple, tags);
490         }, notification.exception);
491     };
493     /**
494      * Add all the event listeners required for keyboard nav, blur clicks etc.
495      *
496      * @method addNavigation
497      * @private
498      * @param {String} inputId The id of the input element for this instance of the autocomplete.
499      * @param {String} suggestionsId The id of the suggestions element for this instance of the autocomplete.
500      * @param {String} downArrowId The id of arrow to open the suggestions list.
501      * @param {String} selectionId The id of element that shows the current selections.
502      * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
503      * @param {Boolean} multiple Are multiple items allowed to be selected?
504      * @param {Boolean} tags Are we allowed to create new items on the fly?
505      */
506     var addNavigation = function(inputId, suggestionsId, downArrowId, selectionId, originalSelect, multiple, tags) {
507         // Start with the input element.
508         var inputElement = $(document.getElementById(inputId));
509         // Add keyboard nav with keydown.
510         inputElement.on('keydown', function(e) {
511             switch (e.keyCode) {
512                 case KEYS.DOWN:
513                     // If the suggestion list is open, move to the next item.
514                     if (inputElement.attr('aria-expanded') === "true") {
515                         activateNextItem(inputId, suggestionsId);
516                     } else {
517                         // Else - open the suggestions list.
518                         updateSuggestions(inputElement.val(), inputId, suggestionsId, originalSelect, multiple, tags);
519                     }
520                     // We handled this event, so prevent it.
521                     e.preventDefault();
522                     return false;
523                 case KEYS.COMMA:
524                     if (tags) {
525                         // If we are allowing tags, comma should create a tag (or enter).
526                         createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
527                     }
528                     // We handled this event, so prevent it.
529                     e.preventDefault();
530                     return false;
531                 case KEYS.UP:
532                     // Choose the previous active item.
533                     activatePreviousItem(inputId, suggestionsId);
534                     // We handled this event, so prevent it.
535                     e.preventDefault();
536                     return false;
537                 case KEYS.ENTER:
538                     var suggestionsElement = $(document.getElementById(suggestionsId));
539                     if ((inputElement.attr('aria-expanded') === "true") &&
540                             (suggestionsElement.children('[aria-selected=true]').length > 0)) {
541                         // If the suggestion list has an active item, select it.
542                         selectCurrentItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
543                     } else if (tags) {
544                         // If tags are enabled, create a tag.
545                         createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
546                     }
547                     // We handled this event, so prevent it.
548                     e.preventDefault();
549                     return false;
550                 case KEYS.ESCAPE:
551                     if (inputElement.attr('aria-expanded') === "true") {
552                         // If the suggestion list is open, close it.
553                         closeSuggestions(inputId, suggestionsId, selectionId);
554                     }
555                     // We handled this event, so prevent it.
556                     e.preventDefault();
557                     return false;
558             }
559             return true;
560         });
561         // Handler used to force set the value from behat.
562         inputElement.on('behat:set-value', function() {
563             if (tags) {
564                 createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
565             }
566         });
567         inputElement.on('blur focus', function(e) {
568             // We may be blurring because we have clicked on the suggestion list. We
569             // dont want to close the selection list before the click event fires, so
570             // we have to delay.
571             if (closeSuggestionsTimer) {
572                 window.clearTimeout(closeSuggestionsTimer);
573             }
574             closeSuggestionsTimer = window.setTimeout(function() {
575                 if ((e.type == 'blur') && tags) {
576                     createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
577                 }
578                 closeSuggestions(inputId, suggestionsId, selectionId);
579             }, 500);
580         });
581         var arrowElement = $(document.getElementById(downArrowId));
582         arrowElement.on('click', function() {
583             // Prevent the close timer, or we will open, then close the suggestions.
584             inputElement.focus();
585             if (closeSuggestionsTimer) {
586                 window.clearTimeout(closeSuggestionsTimer);
587             }
588             // Show the suggestions list.
589             updateSuggestions(inputElement.val(), inputId, suggestionsId, originalSelect, multiple, tags);
590         });
592         var suggestionsElement = $(document.getElementById(suggestionsId));
593         suggestionsElement.parent().on('click', '[role=option]', function(e) {
594             // Handle clicks on suggestions.
595             var element = $(e.currentTarget).closest('[role=option]');
596             var suggestionsElement = $(document.getElementById(suggestionsId));
597             // Find the index of the clicked on suggestion.
598             var current = suggestionsElement.children('[aria-hidden=false]').index(element);
599             // Activate it.
600             activateItem(current, inputId, suggestionsId);
601             // And select it.
602             selectCurrentItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
603         });
604         var selectionElement = $(document.getElementById(selectionId));
605         // Handle clicks on the selected items (will unselect an item).
606         selectionElement.parent().on('click', '[role=listitem]', function(e) {
607             // Get the item that was clicked.
608             var item = $(e.currentTarget);
609             // Remove it from the selection.
610             deselectItem(inputId, selectionId, item, originalSelect, multiple);
611         });
612         // Keyboard navigation for the selection list.
613         selectionElement.parent().on('keydown', function(e) {
614             switch (e.keyCode) {
615                 case KEYS.DOWN:
616                     // Choose the next selection item.
617                     activateNextSelection(selectionId);
618                     // We handled this event, so prevent it.
619                     e.preventDefault();
620                     return false;
621                 case KEYS.UP:
622                     // Choose the previous selection item.
623                     activatePreviousSelection(selectionId);
624                     // We handled this event, so prevent it.
625                     e.preventDefault();
626                     return false;
627                 case KEYS.SPACE:
628                 case KEYS.ENTER:
629                     // Get the item that is currently selected.
630                     var selectedItem = $(document.getElementById(selectionId)).children('[data-active-selection=true]');
631                     if (selectedItem) {
632                         // Unselect this item.
633                         deselectItem(inputId, selectionId, selectedItem, originalSelect, multiple);
634                         // We handled this event, so prevent it.
635                         e.preventDefault();
636                     }
637                     return false;
638             }
639             return true;
640         });
641         // Whenever the input field changes, update the suggestion list.
642         inputElement.on('input', function(e) {
643             var query = $(e.currentTarget).val();
644             updateSuggestions(query, inputId, suggestionsId, originalSelect, multiple, tags);
645         });
646     };
648     return /** @alias module:core/form-autocomplete */ {
649         // Public variables and functions.
650         /**
651          * Turn a boring select box into an auto-complete beast.
652          *
653          * @method enhance
654          * @param {string} select The selector that identifies the select box.
655          * @param {boolean} tags Whether to allow support for tags (can define new entries).
656          * @param {string} ajax Name of an AMD module to handle ajax requests. If specified, the AMD
657          *                      module must expose 2 functions "transport" and "processResults".
658          *                      These are modeled on Select2 see: https://select2.github.io/options.html#ajax
659          * @param {String} placeholder - The text to display before a selection is made.
660          */
661         enhance: function(selector, tags, ajax, placeholder) {
662             // Set some default values.
663             if (typeof tags === "undefined") {
664                 tags = false;
665             }
666             if (typeof ajax === "undefined") {
667                 ajax = false;
668             }
670             // Look for the select element.
671             var originalSelect = $(selector);
672             if (!originalSelect) {
673                 log.debug('Selector not found: ' + selector);
674                 return false;
675             }
677             // Hide the original select.
678             originalSelect.hide().attr('aria-hidden', true);
680             // Find or generate some ids.
681             var selectId = originalSelect.attr('id');
682             var multiple = originalSelect.attr('multiple');
683             var inputId = 'form_autocomplete_input-' + $.now();
684             var suggestionsId = 'form_autocomplete_suggestions-' + $.now();
685             var selectionId = 'form_autocomplete_selection-' + $.now();
686             var downArrowId = 'form_autocomplete_downarrow-' + $.now();
688             var originalLabel = $('[for=' + selectId + ']');
689             // Create the new markup and insert it after the select.
690             var options = [];
691             originalSelect.children('option').each(function(index, option) {
692                 options[index] = { label: option.innerHTML, value: $(option).attr('value') };
693             });
695             // Render all the parts of our UI.
696             var renderInput = templates.render(
697                 'core/form_autocomplete_input',
698                 { downArrowId: downArrowId,
699                   inputId: inputId,
700                   suggestionsId: suggestionsId,
701                   selectionId: selectionId,
702                   placeholder: placeholder,
703                   multiple: multiple }
704             );
705             var renderDatalist = templates.render(
706                 'core/form_autocomplete_suggestions',
707                 { inputId: inputId, suggestionsId: suggestionsId, options: options, multiple: multiple}
708             );
709             var renderSelection = templates.render(
710                 'core/form_autocomplete_selection',
711                 { selectionId: selectionId, items: [], multiple: multiple}
712             );
714             $.when(renderInput, renderDatalist, renderSelection).done(function(input, suggestions, selection) {
715                 // Add our new UI elements to the page.
716                 originalSelect.after(suggestions);
717                 originalSelect.after(input);
718                 originalSelect.after(selection);
719                 // Update the form label to point to the text input.
720                 originalLabel.attr('for', inputId);
721                 // Add the event handlers.
722                 addNavigation(inputId, suggestionsId, downArrowId, selectionId, originalSelect, multiple, tags);
724                 var inputElement = $(document.getElementById(inputId));
725                 var suggestionsElement = $(document.getElementById(suggestionsId));
726                 // Hide the suggestions by default.
727                 suggestionsElement.hide().attr('aria-hidden', true);
729                 // If this field uses ajax, set it up.
730                 if (ajax) {
731                     require([ajax], function(ajaxHandler) {
732                         var handler = function(e) {
733                             updateAjax(e, selector, inputId, suggestionsId, originalSelect, multiple, tags, ajaxHandler);
734                         };
735                         // Trigger an ajax update after the text field value changes.
736                         inputElement.on("input keypress", handler);
737                         var arrowElement = $(document.getElementById(downArrowId));
738                         arrowElement.on("click", handler);
739                     });
740                 }
741                 // Show the current values in the selection list.
742                 updateSelectionList(selectionId, inputId, originalSelect, multiple);
743             });
744         }
745     };
746 });