[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/amd/src/ -> form-autocomplete.js (source)

   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/>.
  15  
  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) {
  28  
  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      };
  39  
  40      /**
  41       * Make an item in the selection list "active".
  42       *
  43       * @method activateSelection
  44       * @private
  45       * @param {Number} index The index in the current (visible) list of selection.
  46       * @param {Object} state State variables for this autocomplete element.
  47       */
  48      var activateSelection = function(index, state) {
  49          // Find the elements in the DOM.
  50          var selectionElement = $(document.getElementById(state.selectionId));
  51  
  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;
  56          while (index < 0) {
  57              index += length;
  58          }
  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;
  63  
  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);
  70      };
  71  
  72      /**
  73       * Update the element that shows the currently selected items.
  74       *
  75       * @method updateSelectionList
  76       * @private
  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.
  80       */
  81      var updateSelectionList = function(options, state, originalSelect) {
  82          // Build up a valid context to re-render the template.
  83          var items = [];
  84          var newSelection = $(document.getElementById(state.selectionId));
  85          var activeId = newSelection.attr('aria-activedescendant');
  86          var activeValue = false;
  87  
  88          if (activeId) {
  89              activeValue = $(document.getElementById(activeId)).attr('data-value');
  90          }
  91          originalSelect.children('option').each(function(index, ele) {
  92              if ($(ele).prop('selected')) {
  93                  items.push({label: $(ele).html(), value: $(ele).attr('value')});
  94              }
  95          });
  96          var context = $.extend({items: items}, options, state);
  97  
  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());
 102  
 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);
 108                      }
 109                  });
 110              }
 111          }).fail(notification.exception);
 112      };
 113  
 114      /**
 115       * Notify of a change in the selection.
 116       *
 117       * @param {jQuery} originalSelect The jQuery object matching the hidden select list.
 118       */
 119      var notifyChange = function(originalSelect) {
 120          if (typeof M.core_formchangechecker !== 'undefined') {
 121              M.core_formchangechecker.set_form_changed();
 122          }
 123          originalSelect.change();
 124      };
 125  
 126      /**
 127       * Remove the given item from the list of selected things.
 128       *
 129       * @method deselectItem
 130       * @private
 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.
 135       */
 136      var deselectItem = function(options, state, item, originalSelect) {
 137          var selectedItemValue = $(item).attr('data-value');
 138  
 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')) {
 147                          $(ele).remove();
 148                      }
 149                  }
 150              });
 151          }
 152          // Rerender the selection list.
 153          updateSelectionList(options, state, originalSelect);
 154          // Notifiy that the selection changed.
 155          notifyChange(originalSelect);
 156      };
 157  
 158      /**
 159       * Make an item in the suggestions "active" (about to be selected).
 160       *
 161       * @method activateItem
 162       * @private
 163       * @param {Number} index The index in the current (visible) list of suggestions.
 164       * @param {Object} state State variables for this instance of autocomplete.
 165       */
 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));
 170  
 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;
 175          while (index < 0) {
 176              index += length;
 177          }
 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;
 184  
 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);
 191  
 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({
 198              scrollTop: scrollPos
 199          }, 100);
 200      };
 201  
 202      /**
 203       * Find the index of the current active suggestion, and activate the next one.
 204       *
 205       * @method activateNextItem
 206       * @private
 207       * @param {Object} state State variable for this auto complete element.
 208       */
 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]');
 214          // Find it's index.
 215          var current = suggestionsElement.children('[aria-hidden=false]').index(element);
 216          // Activate the next one.
 217          activateItem(current + 1, state);
 218      };
 219  
 220      /**
 221       * Find the index of the current active selection, and activate the previous one.
 222       *
 223       * @method activatePreviousSelection
 224       * @private
 225       * @param {Object} state State variables for this instance of autocomplete.
 226       */
 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]');
 232          if (!element) {
 233              activateSelection(0, state);
 234              return;
 235          }
 236          // Find it's index.
 237          var current = selectionsElement.children('[aria-selected=true]').index(element);
 238          // Activate the next one.
 239          activateSelection(current - 1, state);
 240      };
 241      /**
 242       * Find the index of the current active selection, and activate the next one.
 243       *
 244       * @method activateNextSelection
 245       * @private
 246       * @param {Object} state State variables for this instance of autocomplete.
 247       */
 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]');
 253          if (!element) {
 254              activateSelection(0, state);
 255              return;
 256          }
 257          // Find it's index.
 258          var current = selectionsElement.children('[aria-selected=true]').index(element);
 259          // Activate the next one.
 260          activateSelection(current + 1, state);
 261      };
 262  
 263      /**
 264       * Find the index of the current active suggestion, and activate the previous one.
 265       *
 266       * @method activatePreviousItem
 267       * @private
 268       * @param {Object} state State variables for this autocomplete element.
 269       */
 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]');
 275          // Find it's index.
 276          var current = suggestionsElement.children('[aria-hidden=false]').index(element);
 277          // Activate the next one.
 278          activateItem(current - 1, state);
 279      };
 280  
 281      /**
 282       * Close the list of suggestions.
 283       *
 284       * @method closeSuggestions
 285       * @private
 286       * @param {Object} state State variables for this autocomplete element.
 287       */
 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));
 292  
 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);
 297      };
 298  
 299      /**
 300       * Rebuild the list of suggestions based on the current values in the select list, and the query.
 301       *
 302       * @method updateSuggestions
 303       * @private
 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.
 308       */
 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));
 313  
 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')};
 321              }
 322          });
 323  
 324          // Re-render the list of suggestions.
 325          var searchquery = state.caseSensitive ? query : query.toLocaleLowerCase();
 326          var context = $.extend({options: suggestions}, options, state);
 327          templates.render(
 328              'core/form_autocomplete_suggestions',
 329              context
 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) {
 339                  node = $(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;
 344                  } else {
 345                      node.hide().attr('aria-hidden', true);
 346                  }
 347              });
 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.
 354                  if (!options.tags) {
 355                      activateItem(0, state);
 356                  }
 357              } else {
 358                  // Nothing matches. Tell them that.
 359                  str.get_string('nosuggestions', 'form').done(function(nosuggestionsstr) {
 360                      suggestionsElement.html(nosuggestionsstr);
 361                  });
 362              }
 363          }).fail(notification.exception);
 364  
 365      };
 366  
 367      /**
 368       * Create a new item for the list (a tag).
 369       *
 370       * @method createItem
 371       * @private
 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.
 375       */
 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(',');
 382          var found = false;
 383  
 384          $.each(tags, function(tagindex, tag) {
 385              // If we can only select one at a time, deselect any current value.
 386              tag = tag.trim();
 387              if (tag !== '') {
 388                  if (!options.multiple) {
 389                      originalSelect.children('option').prop('selected', false);
 390                  }
 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) {
 394                          found = true;
 395                          $(ele).prop('selected', true);
 396                      }
 397                  });
 398                  // Only create the item if it's new.
 399                  if (!found) {
 400                      var option = $('<option>');
 401                      option.append(tag);
 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);
 407                  }
 408              }
 409          });
 410  
 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);
 418      };
 419  
 420      /**
 421       * Select the currently active item from the suggestions list.
 422       *
 423       * @method selectCurrentItem
 424       * @private
 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.
 428       */
 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.
 434  
 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);
 442          }
 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);
 447              }
 448          });
 449          // Rerender the selection list.
 450          updateSelectionList(options, state, originalSelect);
 451          // Notifiy that the selection changed.
 452          notifyChange(originalSelect);
 453          // Clear the input element.
 454          inputElement.val('');
 455          // Close the list of suggestions.
 456          closeSuggestions(state);
 457      };
 458  
 459      /**
 460       * Fetch a new list of options via ajax.
 461       *
 462       * @method updateAjax
 463       * @private
 464       * @param {Event} e The event that triggered this update.
 465       * @param {Object} options The original options for the autocomplete.
 466       * @param {Object} state The state variables for the autocomplete.
 467       * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
 468       * @param {Object} ajaxHandler This is a module that does the ajax fetch and translates the results.
 469       */
 470      var updateAjax = function(e, options, state, originalSelect, ajaxHandler) {
 471          // Get the query to pass to the ajax function.
 472          var query = $(e.currentTarget).val();
 473          // Call the transport function to do the ajax (name taken from Select2).
 474          ajaxHandler.transport(options.selector, query, function(results) {
 475              // We got a result - pass it through the translator before using it.
 476              var processedResults = ajaxHandler.processResults(options.selector, results);
 477              var existingValues = [];
 478  
 479              // Now destroy all options that are not currently selected.
 480              originalSelect.children('option').each(function(optionIndex, option) {
 481                  option = $(option);
 482                  if (!option.prop('selected')) {
 483                      option.remove();
 484                  } else {
 485                      existingValues.push(String(option.attr('value')));
 486                  }
 487              });
 488  
 489              if (!options.multiple && originalSelect.children('option').length === 0) {
 490                  // If this is a single select - and there are no current options
 491                  // the first option added will be selected by the browser. This causes a bug!
 492                  // We need to insert an empty option so that none of the real options are selected.
 493                  var option = $('<option>');
 494                  originalSelect.append(option);
 495              }
 496              // And add all the new ones returned from ajax.
 497              $.each(processedResults, function(resultIndex, result) {
 498                  if (existingValues.indexOf(String(result.value)) === -1) {
 499                      var option = $('<option>');
 500                      option.append(result.label);
 501                      option.attr('value', result.value);
 502                      originalSelect.append(option);
 503                  }
 504              });
 505              // Update the list of suggestions now from the new values in the select list.
 506              updateSuggestions(options, state, '', originalSelect);
 507          }, notification.exception);
 508      };
 509  
 510      /**
 511       * Add all the event listeners required for keyboard nav, blur clicks etc.
 512       *
 513       * @method addNavigation
 514       * @private
 515       * @param {Object} options The options used to create this autocomplete element.
 516       * @param {Object} state State variables for this autocomplete element.
 517       * @param {JQuery} originalSelect The JQuery object matching the hidden select list.
 518       */
 519      var addNavigation = function(options, state, originalSelect) {
 520          // Start with the input element.
 521          var inputElement = $(document.getElementById(state.inputId));
 522          // Add keyboard nav with keydown.
 523          inputElement.on('keydown', function(e) {
 524              switch (e.keyCode) {
 525                  case KEYS.DOWN:
 526                      // If the suggestion list is open, move to the next item.
 527                      if (!options.showSuggestions) {
 528                          // Do not consume this event.
 529                          return true;
 530                      } else if (inputElement.attr('aria-expanded') === "true") {
 531                          activateNextItem(state);
 532                      } else {
 533                          // Handle ajax population of suggestions.
 534                          if (!inputElement.val() && options.ajax) {
 535                              require([options.ajax], function(ajaxHandler) {
 536                                  updateAjax(e, options, state, originalSelect, ajaxHandler);
 537                              });
 538                          } else {
 539                              // Else - open the suggestions list.
 540                              updateSuggestions(options, state, inputElement.val(), originalSelect);
 541                          }
 542                      }
 543                      // We handled this event, so prevent it.
 544                      e.preventDefault();
 545                      return false;
 546                  case KEYS.COMMA:
 547                      if (options.tags) {
 548                          // If we are allowing tags, comma should create a tag (or enter).
 549                          createItem(options, state, originalSelect);
 550                      }
 551                      // We handled this event, so prevent it.
 552                      e.preventDefault();
 553                      return false;
 554                  case KEYS.UP:
 555                      // Choose the previous active item.
 556                      activatePreviousItem(state);
 557                      // We handled this event, so prevent it.
 558                      e.preventDefault();
 559                      return false;
 560                  case KEYS.ENTER:
 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);
 569                      }
 570                      // We handled this event, so prevent it.
 571                      e.preventDefault();
 572                      return false;
 573                  case KEYS.ESCAPE:
 574                      if (inputElement.attr('aria-expanded') === "true") {
 575                          // If the suggestion list is open, close it.
 576                          closeSuggestions(state);
 577                      }
 578                      // We handled this event, so prevent it.
 579                      e.preventDefault();
 580                      return false;
 581              }
 582              return true;
 583          });
 584          // Handler used to force set the value from behat.
 585          inputElement.on('behat:set-value', function() {
 586              var suggestionsElement = $(document.getElementById(state.suggestionsId));
 587              if ((inputElement.attr('aria-expanded') === "true") &&
 588                      (suggestionsElement.children('[aria-selected=true]').length > 0)) {
 589                  // If the suggestion list has an active item, select it.
 590                  selectCurrentItem(options, state, originalSelect);
 591              } else if (options.tags) {
 592                  // If tags are enabled, create a tag.
 593                  createItem(options, state, originalSelect);
 594              }
 595          });
 596          inputElement.on('blur', function() {
 597              window.setTimeout(function() {
 598                  // Get the current element with focus.
 599                  var focusElement = $(document.activeElement);
 600                  // Only close the menu if the input hasn't regained focus.
 601                  if (focusElement.attr('id') != inputElement.attr('id')) {
 602                      if (options.tags) {
 603                          createItem(options, state, originalSelect);
 604                      }
 605                      closeSuggestions(state);
 606                  }
 607              }, 500);
 608          });
 609          if (options.showSuggestions) {
 610              var arrowElement = $(document.getElementById(state.downArrowId));
 611              arrowElement.on('click', function() {
 612                  // Prevent the close timer, or we will open, then close the suggestions.
 613                  inputElement.focus();
 614                  // Show the suggestions list.
 615                  updateSuggestions(options, state, inputElement.val(), originalSelect);
 616              });
 617          }
 618  
 619          var suggestionsElement = $(document.getElementById(state.suggestionsId));
 620          suggestionsElement.parent().on('click', '[role=option]', function(e) {
 621              // Handle clicks on suggestions.
 622              var element = $(e.currentTarget).closest('[role=option]');
 623              var suggestionsElement = $(document.getElementById(state.suggestionsId));
 624              // Find the index of the clicked on suggestion.
 625              var current = suggestionsElement.children('[aria-hidden=false]').index(element);
 626              // Activate it.
 627              activateItem(current, state);
 628              // And select it.
 629              selectCurrentItem(options, state, originalSelect);
 630          });
 631          var selectionElement = $(document.getElementById(state.selectionId));
 632          // Handle clicks on the selected items (will unselect an item).
 633          selectionElement.on('click', '[role=listitem]', function(e) {
 634              // Get the item that was clicked.
 635              var item = $(e.currentTarget);
 636              // Remove it from the selection.
 637              deselectItem(options, state, item, originalSelect);
 638          });
 639          // Keyboard navigation for the selection list.
 640          selectionElement.on('keydown', function(e) {
 641              switch (e.keyCode) {
 642                  case KEYS.DOWN:
 643                      // Choose the next selection item.
 644                      activateNextSelection(state);
 645                      // We handled this event, so prevent it.
 646                      e.preventDefault();
 647                      return false;
 648                  case KEYS.UP:
 649                      // Choose the previous selection item.
 650                      activatePreviousSelection(state);
 651                      // We handled this event, so prevent it.
 652                      e.preventDefault();
 653                      return false;
 654                  case KEYS.SPACE:
 655                  case KEYS.ENTER:
 656                      // Get the item that is currently selected.
 657                      var selectedItem = $(document.getElementById(state.selectionId)).children('[data-active-selection=true]');
 658                      if (selectedItem) {
 659                          // Unselect this item.
 660                          deselectItem(options, state, selectedItem, originalSelect);
 661                          // We handled this event, so prevent it.
 662                          e.preventDefault();
 663                      }
 664                      return false;
 665              }
 666              return true;
 667          });
 668          // Whenever the input field changes, update the suggestion list.
 669          if (options.showSuggestions) {
 670              inputElement.on('input', function(e) {
 671                  var query = $(e.currentTarget).val();
 672                  var last = $(e.currentTarget).data('last-value');
 673                  // IE11 fires many more input events than required - even when the value has not changed.
 674                  // We need to only do this for real value changed events or the suggestions will be
 675                  // unclickable on IE11 (because they will be rebuilt before the click event fires).
 676                  // Note - because of this we cannot close the list when the query is empty or it will break
 677                  // on IE11.
 678                  if (last !== query) {
 679                      updateSuggestions(options, state, query, originalSelect);
 680                  }
 681                  $(e.currentTarget).data('last-value', query);
 682              });
 683          }
 684      };
 685  
 686      return /** @alias module:core/form-autocomplete */ {
 687          // Public variables and functions.
 688          /**
 689           * Turn a boring select box into an auto-complete beast.
 690           *
 691           * @method enhance
 692           * @param {string} selector The selector that identifies the select box.
 693           * @param {boolean} tags Whether to allow support for tags (can define new entries).
 694           * @param {string} ajax Name of an AMD module to handle ajax requests. If specified, the AMD
 695           *                      module must expose 2 functions "transport" and "processResults".
 696           *                      These are modeled on Select2 see: https://select2.github.io/options.html#ajax
 697           * @param {String} placeholder - The text to display before a selection is made.
 698           * @param {Boolean} caseSensitive - If search has to be made case sensitive.
 699           * @param {Boolean} showSuggestions - If suggestions should be shown
 700           * @param {String} noSelectionString - Text to display when there is no selection
 701           */
 702          enhance: function(selector, tags, ajax, placeholder, caseSensitive, showSuggestions, noSelectionString) {
 703              // Set some default values.
 704              var options = {
 705                  selector: selector,
 706                  tags: false,
 707                  ajax: false,
 708                  placeholder: placeholder,
 709                  caseSensitive: false,
 710                  showSuggestions: true,
 711                  noSelectionString: noSelectionString
 712              };
 713              if (typeof tags !== "undefined") {
 714                  options.tags = tags;
 715              }
 716              if (typeof ajax !== "undefined") {
 717                  options.ajax = ajax;
 718              }
 719              if (typeof caseSensitive !== "undefined") {
 720                  options.caseSensitive = caseSensitive;
 721              }
 722              if (typeof showSuggestions !== "undefined") {
 723                  options.showSuggestions = showSuggestions;
 724              }
 725              if (typeof noSelectionString === "undefined") {
 726                  str.get_string('noselection', 'form').done(function(result) {
 727                      options.noSelectionString = result;
 728                  }).fail(notification.exception);
 729              }
 730  
 731              // Look for the select element.
 732              var originalSelect = $(selector);
 733              if (!originalSelect) {
 734                  log.debug('Selector not found: ' + selector);
 735                  return;
 736              }
 737  
 738              // Hide the original select.
 739              originalSelect.hide().attr('aria-hidden', true);
 740  
 741              // Find or generate some ids.
 742              var state = {
 743                  selectId: originalSelect.attr('id'),
 744                  inputId: 'form_autocomplete_input-' + $.now(),
 745                  suggestionsId: 'form_autocomplete_suggestions-' + $.now(),
 746                  selectionId: 'form_autocomplete_selection-' + $.now(),
 747                  downArrowId: 'form_autocomplete_downarrow-' + $.now()
 748              };
 749              options.multiple = originalSelect.attr('multiple');
 750  
 751              var originalLabel = $('[for=' + state.selectId + ']');
 752              // Create the new markup and insert it after the select.
 753              var suggestions = [];
 754              originalSelect.children('option').each(function(index, option) {
 755                  suggestions[index] = {label: option.innerHTML, value: $(option).attr('value')};
 756              });
 757  
 758              // Render all the parts of our UI.
 759              var context = $.extend({}, options, state);
 760              context.options = suggestions;
 761              context.items = [];
 762  
 763              var renderInput = templates.render('core/form_autocomplete_input', context);
 764              var renderDatalist = templates.render('core/form_autocomplete_suggestions', context);
 765              var renderSelection = templates.render('core/form_autocomplete_selection', context);
 766  
 767              $.when(renderInput, renderDatalist, renderSelection).done(function(input, suggestions, selection) {
 768                  // Add our new UI elements to the page.
 769                  originalSelect.after(suggestions);
 770                  originalSelect.after(input);
 771                  originalSelect.after(selection);
 772                  // Update the form label to point to the text input.
 773                  originalLabel.attr('for', state.inputId);
 774                  // Add the event handlers.
 775                  addNavigation(options, state, originalSelect);
 776  
 777                  var inputElement = $(document.getElementById(state.inputId));
 778                  var suggestionsElement = $(document.getElementById(state.suggestionsId));
 779                  // Hide the suggestions by default.
 780                  suggestionsElement.hide().attr('aria-hidden', true);
 781  
 782                  // If this field uses ajax, set it up.
 783                  if (options.ajax) {
 784                      require([options.ajax], function(ajaxHandler) {
 785                          var throttleTimeout = null;
 786                          var handler = function(e) {
 787                              updateAjax(e, options, state, originalSelect, ajaxHandler);
 788                          };
 789  
 790                          // For input events, we do not want to trigger many, many updates.
 791                          var throttledHandler = function(e) {
 792                              if (throttleTimeout !== null) {
 793                                  window.clearTimeout(throttleTimeout);
 794                                  throttleTimeout = null;
 795                              }
 796                              throttleTimeout = window.setTimeout(handler.bind(this, e), 300);
 797                          };
 798                          // Trigger an ajax update after the text field value changes.
 799                          inputElement.on("input", throttledHandler);
 800                          var arrowElement = $(document.getElementById(state.downArrowId));
 801                          arrowElement.on("click", handler);
 802                      });
 803                  }
 804                  // Show the current values in the selection list.
 805                  updateSelectionList(options, state, originalSelect);
 806              });
 807          }
 808      };
 809  });


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1