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. */
40 /** @var {Number} closeSuggestionsTimer - integer used to cancel window.setTimeout. */
41 var closeSuggestionsTimer = null;
44 * Make an item in the selection list "active".
46 * @method activateSelection
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.
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;
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);
76 * Remove the given item from the list of selected things.
78 * @method deselectItem
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.
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.
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);
98 // Rerender the selection list.
99 updateSelectionList(selectionId, inputId, originalSelect, multiple);
103 * Make an item in the suggestions "active" (about to be selected).
105 * @method activateItem
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.
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;
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);
139 * Find the index of the current active suggestion, and activate the next one.
141 * @method activateNextItem
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.
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]');
152 var current = suggestionsElement.children('[aria-hidden=false]').index(element);
153 // Activate the next one.
154 activateItem(current+1, inputId, suggestionsId);
158 * Find the index of the current active selection, and activate the previous one.
160 * @method activatePreviousSelection
162 * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
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]');
170 activateSelection(0, selectionId);
174 var current = selectionsElement.children('[aria-selected=true]').index(element);
175 // Activate the next one.
176 activateSelection(current-1, selectionId);
179 * Find the index of the current active selection, and activate the next one.
181 * @method activateNextSelection
183 * @param {String} selectionId The id of the selection element for this instance of the autocomplete.
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]');
191 activateSelection(0, selectionId);
195 var current = selectionsElement.children('[aria-selected=true]').index(element);
196 // Activate the next one.
197 activateSelection(current+1, selectionId);
201 * Find the index of the current active suggestion, and activate the previous one.
203 * @method activatePreviousItem
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.
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]');
214 var current = suggestionsElement.children('[aria-hidden=false]').index(element);
215 // Activate the next one.
216 activateItem(current-1, inputId, suggestionsId);
220 * Close the list of suggestions.
222 * @method closeSuggestions
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.
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);
239 * Rebuild the list of suggestions based on the current values in the select list, and the query.
241 * @method updateSuggestions
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?
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.
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') };
265 // Re-render the list of suggestions.
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) {
279 if (node.text().indexOf(query) > -1) {
280 node.show().attr('aria-hidden', false);
281 matchingElements = true;
283 node.hide().attr('aria-hidden', true);
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.
293 activateItem(0, inputId, suggestionsId);
296 // Abort - nothing matches. Hide the suggestions properly.
297 suggestionsElement.hide();
298 suggestionsElement.attr('aria-hidden', true);
299 inputElement.attr('aria-expanded', false);
301 }).fail(notification.exception);
306 * Create a new item for the list (a tag).
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.
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(',');
323 $.each(tags, function(tagindex, tag) {
324 // If we can only select one at a time, deselect any current value.
328 originalSelect.children('option').prop('selected', false);
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) {
334 $(ele).prop('selected', true);
337 // Only create the item if it's new.
339 var option = $('<option>');
341 option.attr('value', tag);
342 originalSelect.append(option);
343 option.prop('selected', true);
347 // Get the selection element.
348 var newSelection = $(document.getElementById(selectionId));
349 // Build up a valid context to re-render the selection.
351 originalSelect.children('option').each(function(index, ele) {
352 if ($(ele).prop('selected')) {
353 items.push( { label: $(ele).html(), value: $(ele).attr('value') } );
357 selectionId: selectionId,
361 // Re-render the selection.
362 templates.render('core/form_autocomplete_selection', context).done(function(newHTML) {
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();
375 * Update the element that shows the currently selected items.
377 * @method updateSelectionList
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.
384 var updateSelectionList = function(selectionId, inputId, originalSelect, multiple) {
385 // Build up a valid context to re-render the template.
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') } );
394 selectionId: selectionId,
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();
409 * Select the currently active item from the suggestions list.
411 * @method selectCurrentItem
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.
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.
431 originalSelect.children('option').prop('selected', false);
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);
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);
448 * Fetch a new list of options via ajax.
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.
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) {
473 if (!option.prop('selected')) {
476 existingValues.push(option.attr('value'));
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);
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);
494 * Add all the event listeners required for keyboard nav, blur clicks etc.
496 * @method addNavigation
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?
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) {
513 // If the suggestion list is open, move to the next item.
514 if (inputElement.attr('aria-expanded') === "true") {
515 activateNextItem(inputId, suggestionsId);
517 // Else - open the suggestions list.
518 updateSuggestions(inputElement.val(), inputId, suggestionsId, originalSelect, multiple, tags);
520 // We handled this event, so prevent it.
525 // If we are allowing tags, comma should create a tag (or enter).
526 createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
528 // We handled this event, so prevent it.
532 // Choose the previous active item.
533 activatePreviousItem(inputId, suggestionsId);
534 // We handled this event, so prevent it.
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);
544 // If tags are enabled, create a tag.
545 createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
547 // We handled this event, so prevent it.
551 if (inputElement.attr('aria-expanded') === "true") {
552 // If the suggestion list is open, close it.
553 closeSuggestions(inputId, suggestionsId, selectionId);
555 // We handled this event, so prevent it.
561 // Handler used to force set the value from behat.
562 inputElement.on('behat:set-value', function() {
564 createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
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
571 if (closeSuggestionsTimer) {
572 window.clearTimeout(closeSuggestionsTimer);
574 closeSuggestionsTimer = window.setTimeout(function() {
575 if ((e.type == 'blur') && tags) {
576 createItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
578 closeSuggestions(inputId, suggestionsId, selectionId);
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);
588 // Show the suggestions list.
589 updateSuggestions(inputElement.val(), inputId, suggestionsId, originalSelect, multiple, tags);
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);
600 activateItem(current, inputId, suggestionsId);
602 selectCurrentItem(inputId, suggestionsId, selectionId, multiple, originalSelect);
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);
612 // Keyboard navigation for the selection list.
613 selectionElement.parent().on('keydown', function(e) {
616 // Choose the next selection item.
617 activateNextSelection(selectionId);
618 // We handled this event, so prevent it.
622 // Choose the previous selection item.
623 activatePreviousSelection(selectionId);
624 // We handled this event, so prevent it.
629 // Get the item that is currently selected.
630 var selectedItem = $(document.getElementById(selectionId)).children('[data-active-selection=true]');
632 // Unselect this item.
633 deselectItem(inputId, selectionId, selectedItem, originalSelect, multiple);
634 // We handled this event, so prevent it.
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);
648 return /** @alias module:core/form-autocomplete */ {
649 // Public variables and functions.
651 * Turn a boring select box into an auto-complete beast.
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.
661 enhance: function(selector, tags, ajax, placeholder) {
662 // Set some default values.
663 if (typeof tags === "undefined") {
666 if (typeof ajax === "undefined") {
670 // Look for the select element.
671 var originalSelect = $(selector);
672 if (!originalSelect) {
673 log.debug('Selector not found: ' + selector);
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.
691 originalSelect.children('option').each(function(index, option) {
692 options[index] = { label: option.innerHTML, value: $(option).attr('value') };
695 // Render all the parts of our UI.
696 var renderInput = templates.render(
697 'core/form_autocomplete_input',
698 { downArrowId: downArrowId,
700 suggestionsId: suggestionsId,
701 selectionId: selectionId,
702 placeholder: placeholder,
705 var renderDatalist = templates.render(
706 'core/form_autocomplete_suggestions',
707 { inputId: inputId, suggestionsId: suggestionsId, options: options, multiple: multiple}
709 var renderSelection = templates.render(
710 'core/form_autocomplete_selection',
711 { selectionId: selectionId, items: [], multiple: multiple}
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.
731 require([ajax], function(ajaxHandler) {
732 var handler = function(e) {
733 updateAjax(e, selector, inputId, suggestionsId, originalSelect, multiple, tags, ajaxHandler);
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);
741 // Show the current values in the selection list.
742 updateSelectionList(selectionId, inputId, originalSelect, multiple);