From 02a10b31610fa1ef576057947e5c275f517fe908 Mon Sep 17 00:00:00 2001
From: webchick <webchick@24967.no-reply.drupal.org>
Date: Sun, 24 Nov 2013 12:26:30 -0800
Subject: [PATCH] Issue #675446 by mgifford, RobLoach, amateescu, nod_,
 longwave, oxyc, rteijeiro, tomyouds, Jelle_S, mcrittenden, Sutharsan, hansyg,
 Angry Dan, clemens.tolboom, droplet | Dave Reid: Change notice: Use jQuery UI
 Autocomplete.

---
 core/includes/form.inc                        |  16 +-
 core/misc/autocomplete.js                     | 442 ++++++------------
 .../EntityReferenceAutocomplete.php           |   2 +-
 .../Tests/EntityReferenceAutocompleteTest.php |  20 +-
 .../Drupal/node/Tests/NodeCreationTest.php    |   5 +-
 core/modules/system/css/system.module.css     |  23 +-
 core/modules/system/css/system.theme.css      |   3 +-
 .../Drupal/system/Tests/Form/ElementTest.php  |  12 +-
 core/modules/system/system.module             |   2 +
 .../Controller/TermAutocompleteController.php |   2 +-
 .../lib/Drupal/taxonomy/Tests/TermTest.php    |  18 +-
 .../Tests/Views/TaxonomyIndexTidUiTest.php    |   4 +-
 .../user/Tests/UserAutocompleteTest.php       |   4 +-
 .../user/lib/Drupal/user/UserAutocomplete.php |   5 +-
 .../Tests/ViewsTaxonomyAutocompleteTest.php   |   6 +-
 core/themes/bartik/css/style.css              |   4 +-
 core/themes/seven/style.css                   |   1 -
 17 files changed, 207 insertions(+), 362 deletions(-)

diff --git a/core/includes/form.inc b/core/includes/form.inc
index c82124a4f6c2..d12a9b948d2a 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -2158,20 +2158,8 @@ function form_process_autocomplete($element, &$form_state) {
   if ($access) {
     $element['#attributes']['class'][] = 'form-autocomplete';
     $element['#attached']['library'][] = array('system', 'drupal.autocomplete');
-    // Provide a hidden element for the JavaScript behavior to bind to. Since
-    // this element is for client-side functionality only, do not process input.
-    // @todo Refactor autocomplete.js to accept drupalSettings instead of
-    //   requiring extraneous markup.
-    $element['autocomplete'] = array(
-      '#type' => 'hidden',
-      '#input' => FALSE,
-      '#value' => $path,
-      '#disabled' => TRUE,
-      '#attributes' => array(
-        'class' => array('autocomplete'),
-        'id' => $element['#id'] . '-autocomplete',
-      ),
-    );
+    // Provide a data attribute for the JavaScript behavior to bind to.
+    $element['#attributes']['data-autocomplete-path'] = $path;
   }
   return $element;
 }
diff --git a/core/misc/autocomplete.js b/core/misc/autocomplete.js
index 0378b9525d62..ad9bb1cdf84d 100644
--- a/core/misc/autocomplete.js
+++ b/core/misc/autocomplete.js
@@ -1,338 +1,200 @@
-(function ($) {
+(function ($, Drupal) {
 
 "use strict";
 
-/**
- * Attaches the autocomplete behavior to all required fields.
- */
-Drupal.behaviors.autocomplete = {
-  attach: function (context, settings) {
-    var acdb = [];
-    $(context).find('input.autocomplete').once('autocomplete', function () {
-      var uri = this.value;
-      if (!acdb[uri]) {
-        acdb[uri] = new Drupal.ACDB(uri);
-      }
-      var $input = $('#' + this.id.substr(0, this.id.length - 13))
-        .prop('autocomplete', 'OFF')
-        .attr('aria-autocomplete', 'list');
-      $($input[0].form).submit(Drupal.autocompleteSubmit);
-      $input.parent()
-        .attr('role', 'application')
-        .append($('<span class="visually-hidden" aria-live="assertive"></span>')
-          .attr('id', $input[0].id + '-autocomplete-aria-live')
-        );
-      new Drupal.jsAC($input, acdb[uri]);
-    });
-  }
-};
+var autocomplete;
 
 /**
- * Prevents the form from submitting if the suggestions popup is open
- * and closes the suggestions popup when doing so.
+ * Helper splitting terms from the autocomplete value.
+ *
+ * @param {String} value
+ *
+ * @return {Array}
  */
-Drupal.autocompleteSubmit = function () {
-  var $autocomplete = $('#autocomplete');
-  if ($autocomplete.length !== 0) {
-    $autocomplete[0].owner.hidePopup();
-  }
-  return $autocomplete.length === 0;
-};
+function autocompleteSplitValues (value) {
+  // We will match the value against comma-seperated terms.
+  var result = [];
+  var quote = false;
+  var current = '';
+  var valueLength = value.length;
+  var i, character;
 
-/**
- * An AutoComplete object.
- */
-Drupal.jsAC = function ($input, db) {
-  var ac = this;
-  this.input = $input[0];
-  this.ariaLive = $('#' + this.input.id + '-autocomplete-aria-live');
-  this.db = db;
-
-  $input
-    .keydown(function (event) { return ac.onkeydown(this, event); })
-    .keyup(function (event) { ac.onkeyup(this, event); })
-    .blur(function () { ac.hidePopup(); ac.db.cancel(); });
-};
-
-/**
- * Handler for the "keydown" event.
- */
-Drupal.jsAC.prototype.onkeydown = function (input, e) {
-  if (!e) {
-    e = window.event;
+  for (i = 0; i < valueLength; i++) {
+    character = value.charAt(i);
+    if (character === '"') {
+      current += character;
+      quote = !quote;
+    }
+    else if (character === ',' && !quote) {
+      result.push(current.trim());
+      current = '';
+    }
+    else {
+      current += character;
+    }
   }
-  switch (e.keyCode) {
-    case 40: // down arrow.
-      e.preventDefault();
-      this.selectDown();
-      break;
-    case 38: // up arrow.
-      e.preventDefault();
-      this.selectUp();
-      break;
-    default: // All other keys.
-      return true;
+  if (value.length > 0) {
+    result.push($.trim(current));
   }
-};
+
+  return result;
+}
 
 /**
- * Handler for the "keyup" event.
+ * Returns the last value of an multi-value textfield.
+ *
+ * @param {String} terms
+ *
+ * @return {String}
  */
-Drupal.jsAC.prototype.onkeyup = function (input, e) {
-  if (!e) {
-    e = window.event;
-  }
-  switch (e.keyCode) {
-    case 16: // Shift.
-    case 17: // Ctrl.
-    case 18: // Alt.
-    case 20: // Caps lock.
-    case 33: // Page up.
-    case 34: // Page down.
-    case 35: // End.
-    case 36: // Home.
-    case 37: // Left arrow.
-    case 38: // Up arrow.
-    case 39: // Right arrow.
-    case 40: // Down arrow.
-      return true;
-
-    case 9:  // Tab.
-    case 13: // Enter.
-    case 27: // Esc.
-      this.hidePopup(e.keyCode);
-      return true;
-
-    default: // All other keys.
-      if (input.value.length > 0 && !input.readOnly) {
-        this.populatePopup();
-      }
-      else {
-        this.hidePopup(e.keyCode);
-      }
-      return true;
-  }
-};
+function extractLastTerm (terms) {
+  return autocomplete.splitValues(terms).pop();
+}
 
 /**
- * Puts the currently highlighted suggestion into the autocomplete field.
+ * The search handler is called before a search is performed.
+ *
+ * @param {Object} event
+ *
+ * @return {Boolean}
  */
-Drupal.jsAC.prototype.select = function (node) {
-  this.input.value = $(node).data('autocompleteValue');
-};
+function searchHandler (event) {
+  // Only search when the term is two characters or larger.
+  var term = autocomplete.extractLastTerm(event.target.value);
+  return term.length >= autocomplete.minLength;
+}
 
 /**
- * Highlights the next suggestion.
+ * jQuery UI autocomplete source callback.
+ *
+ * @param {Object} request
+ * @param {Function} response
  */
-Drupal.jsAC.prototype.selectDown = function () {
-  if (this.selected && this.selected.nextSibling) {
-    this.highlight(this.selected.nextSibling);
+function sourceData (request, response) {
+  var elementId = this.element.attr('id');
+
+  if (!(elementId in autocomplete.cache)) {
+    autocomplete.cache[elementId] = {};
   }
-  else if (this.popup) {
-    var lis = $(this.popup).find('li');
-    if (lis.length > 0) {
-      this.highlight(lis.get(0));
+
+  /**
+   * Filter through the suggestions removing all terms already tagged and
+   * display the available terms to the user.
+   *
+   * @param {Object} suggestions
+   */
+  function showSuggestions (suggestions) {
+    var tagged = autocomplete.splitValues(request.term);
+    for (var i = 0, il = tagged.length; i < il; i++) {
+      var index = suggestions.indexOf(tagged[i]);
+      if (index >= 0) {
+        suggestions.splice(index, 1);
+      }
     }
+    response(suggestions);
   }
-};
 
-/**
- * Highlights the previous suggestion.
- */
-Drupal.jsAC.prototype.selectUp = function () {
-  if (this.selected && this.selected.previousSibling) {
-    this.highlight(this.selected.previousSibling);
-  }
-};
+  /**
+   * Transforms the data object into an array and update autocomplete results.
+   *
+   * @param {Object} data
+   */
+  function sourceCallbackHandler (data) {
+    autocomplete.cache[elementId][term] = data;
 
-/**
- * Highlights a suggestion.
- */
-Drupal.jsAC.prototype.highlight = function (node) {
-  // Unhighlights a suggestion for "keyup" and "keydown" events.
-  if (this.selected !== false) {
-    $(this.selected).removeClass('selected');
+    // Send the new string array of terms to the jQuery UI list.
+    showSuggestions(data);
   }
-  $(node).addClass('selected');
-  this.selected = node;
-  $(this.ariaLive).html($(this.selected).html());
-};
 
-/**
- * Unhighlights a suggestion.
- */
-Drupal.jsAC.prototype.unhighlight = function (node) {
-  $(node).removeClass('selected');
-  this.selected = false;
-  $(this.ariaLive).empty();
-};
+  // Get the desired term and construct the autocomplete URL for it.
+  var term = autocomplete.extractLastTerm(request.term);
 
-/**
- * Hides the autocomplete suggestions.
- */
-Drupal.jsAC.prototype.hidePopup = function (keycode) {
-  // Select item if the right key or mousebutton was pressed.
-  if (this.selected && ((keycode && keycode !== 46 && keycode !== 8 && keycode !== 27) || !keycode)) {
-    this.input.value = $(this.selected).data('autocompleteValue');
+  // Check if the term is already cached.
+  if (autocomplete.cache[elementId].hasOwnProperty(term)) {
+    showSuggestions(autocomplete.cache[elementId][term]);
   }
-  // Hide popup.
-  var popup = this.popup;
-  if (popup) {
-    this.popup = null;
-    $(popup).fadeOut('fast', function () { $(popup).remove(); });
+  else {
+    var options = $.extend({ success: sourceCallbackHandler, data: { q: term } }, autocomplete.ajax);
+    /*jshint validthis:true */
+    $.ajax(this.element.attr('data-autocomplete-path'), options);
   }
-  this.selected = false;
-  $(this.ariaLive).empty();
-};
+}
 
 /**
- * Positions the suggestions popup and starts a search.
+ * Handles an autocompletefocus event.
+ *
+ * @return {Boolean}
  */
-Drupal.jsAC.prototype.populatePopup = function () {
-  var $input = $(this.input);
-  var position = $input.position();
-  // Show popup.
-  if (this.popup) {
-    $(this.popup).remove();
-  }
-  this.selected = false;
-  this.popup = $('<div id="autocomplete"></div>')[0];
-  this.popup.owner = this;
-  $(this.popup).css({
-    top: parseInt(position.top + this.input.offsetHeight, 10) + 'px',
-    left: parseInt(position.left, 10) + 'px',
-    width: $input.innerWidth() + 'px',
-    display: 'none'
-  });
-  $input.before(this.popup);
-
-  // Do search.
-  this.db.owner = this;
-  this.db.search(this.input.value);
-};
+function focusHandler () {
+  return false;
+}
 
 /**
- * Fills the suggestion popup with any matches received.
+ * Handles an autocompleteselect event.
+ *
+ * @param {Object} event
+ * @param {Object} ui
+ *
+ * @return {Boolean}
  */
-Drupal.jsAC.prototype.found = function (matches) {
-  // If no value in the textfield, do not show the popup.
-  if (!this.input.value.length) {
-    return false;
+function selectHandler (event, ui) {
+  var terms = autocomplete.splitValues(event.target.value);
+  // Remove the current input.
+  terms.pop();
+  // Add the selected item.
+  if (ui.item.value.search(",") > 0) {
+    terms.push('"' + ui.item.value + '"');
   }
-
-  // Prepare matches.
-  var ac = this;
-  var ul = $('<ul></ul>')
-    .on('mousedown', 'li', function (e) { ac.select(this); })
-    .on('mouseover', 'li', function (e) { ac.highlight(this); })
-    .on('mouseout', 'li', function (e) { ac.unhighlight(this); });
-  for (var key in matches) {
-    if (matches.hasOwnProperty(key)) {
-      $('<li></li>')
-        .html($('<div></div>').html(matches[key]))
-        .data('autocompleteValue', key)
-        .appendTo(ul);
-    }
+  else {
+    terms.push(ui.item.value);
   }
+  event.target.value = terms.join(', ');
+  // Return false to tell jQuery UI that we've filled in the value already.
+  return false;
+}
 
-  // Show popup with matches, if any.
-  if (this.popup) {
-    if (ul.children().length) {
-      $(this.popup).empty().append(ul).show();
-      $(this.ariaLive).html(Drupal.t('Autocomplete popup'));
+/**
+ * Attaches the autocomplete behavior to all required fields.
+ */
+Drupal.behaviors.autocomplete = {
+  attach: function (context) {
+    // Act on textfields with the "form-autocomplete" class.
+    var $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete');
+    if ($autocomplete.length) {
+      // Use jQuery UI Autocomplete on the textfield.
+      $autocomplete.autocomplete(autocomplete.options);
     }
-    else {
-      $(this.popup).css({ visibility: 'hidden' });
-      this.hidePopup();
+  },
+  detach: function (context, settings, trigger) {
+    if (trigger === 'unload') {
+      $(context).find('input.form-autocomplete')
+        .removeOnce('autocomplete')
+        .autocomplete('destroy');
     }
   }
 };
 
-Drupal.jsAC.prototype.setStatus = function (status) {
-  switch (status) {
-    case 'begin':
-      $(this.input).addClass('throbbing');
-      $(this.ariaLive).html(Drupal.t('Searching for matches...'));
-      break;
-    case 'cancel':
-    case 'error':
-    case 'found':
-      $(this.input).removeClass('throbbing');
-      break;
-  }
-};
-
-/**
- * An AutoComplete DataBase object.
- */
-Drupal.ACDB = function (uri) {
-  this.uri = uri;
-  this.delay = 300;
-  this.cache = {};
-};
-
 /**
- * Performs a cached and delayed search.
+ * Autocomplete object implementation.
  */
-Drupal.ACDB.prototype.search = function (searchString) {
-  var db = this;
-  this.searchString = searchString;
-
-  // See if this string needs to be searched for anyway.
-  searchString = searchString.replace(/^\s+|\s+$/, '');
-  if (searchString.length <= 0 ||
-    searchString.charAt(searchString.length - 1) === ',') {
-    return;
-  }
-
-  // See if this key has been searched for before.
-  if (this.cache[searchString]) {
-    return this.owner.found(this.cache[searchString]);
+autocomplete = {
+  cache: {},
+  // Exposes methods to allow overriding by contrib.
+  minLength: 1,
+  splitValues: autocompleteSplitValues,
+  extractLastTerm: extractLastTerm,
+  // jQuery UI autocomplete options.
+  options: {
+    source: sourceData,
+    focus: focusHandler,
+    search: searchHandler,
+    select: selectHandler
+  },
+  ajax: {
+    dataType: 'json'
   }
-
-  // Initiate delayed search.
-  if (this.timer) {
-    clearTimeout(this.timer);
-  }
-  this.timer = setTimeout(function () {
-    db.owner.setStatus('begin');
-
-    // Ajax GET request for autocompletion.
-    $.ajax({
-      type: 'GET',
-      url: db.uri,
-      data: {
-        q: searchString
-      },
-      dataType: 'json',
-      success: function (matches) {
-        if (typeof matches.status === 'undefined' || matches.status !== 0) {
-          db.cache[searchString] = matches;
-          // Verify if these are still the matches the user wants to see.
-          if (db.searchString === searchString) {
-            db.owner.found(matches);
-          }
-          db.owner.setStatus('found');
-        }
-      },
-      error: function (xmlhttp) {
-        throw new Drupal.AjaxError(xmlhttp, db.uri);
-      }
-    });
-  }, this.delay);
 };
 
-/**
- * Cancels the current autocomplete request.
- */
-Drupal.ACDB.prototype.cancel = function () {
-  if (this.owner) {
-    this.owner.setStatus('cancel');
-  }
-  if (this.timer) {
-    clearTimeout(this.timer);
-  }
-  this.searchString = '';
-};
+Drupal.autocomplete = autocomplete;
 
-})(jQuery);
+})(jQuery, Drupal);
diff --git a/core/modules/entity_reference/lib/Drupal/entity_reference/EntityReferenceAutocomplete.php b/core/modules/entity_reference/lib/Drupal/entity_reference/EntityReferenceAutocomplete.php
index 56ce263635fc..96909a905305 100644
--- a/core/modules/entity_reference/lib/Drupal/entity_reference/EntityReferenceAutocomplete.php
+++ b/core/modules/entity_reference/lib/Drupal/entity_reference/EntityReferenceAutocomplete.php
@@ -100,7 +100,7 @@ public function getMatches($field, $instance, $entity_type, $entity_id = '', $pr
           if (strpos($key, ',') !== FALSE || strpos($key, '"') !== FALSE) {
             $key = '"' . str_replace('"', '""', $key) . '"';
           }
-          $matches[$prefix . $key] = $label;
+          $matches[] = array('value' => $prefix . $key, 'label' => $label);
         }
       }
     }
diff --git a/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutocompleteTest.php b/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutocompleteTest.php
index 45844ceecea1..98953685ca9d 100644
--- a/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutocompleteTest.php
+++ b/core/modules/entity_reference/lib/Drupal/entity_reference/Tests/EntityReferenceAutocompleteTest.php
@@ -79,21 +79,24 @@ function testEntityReferenceAutocompletion() {
     // We should get both entities in a JSON encoded string.
     $input = '10/';
     $data = $this->getAutocompleteResult('single', $input);
-    $this->assertIdentical($data[$entity_1->name->value . ' (1)'], check_plain($entity_1->name->value), 'Autocomplete returned the first matching entity');
-    $this->assertIdentical($data[$entity_2->name->value . ' (2)'], check_plain($entity_2->name->value), 'Autocomplete returned the second matching entity');
+    $this->assertIdentical($data[0]['label'], check_plain($entity_1->name->value), 'Autocomplete returned the first matching entity');
+    $this->assertIdentical($data[1]['label'], check_plain($entity_2->name->value), 'Autocomplete returned the second matching entity');
 
     // Try to autocomplete a entity label that matches the first entity.
     // We should only get the first entity in a JSON encoded string.
     $input = '10/16';
     $data = $this->getAutocompleteResult('single', $input);
-    $target = array($entity_1->name->value . ' (1)' => check_plain($entity_1->name->value));
-    $this->assertIdentical($data, $target, 'Autocomplete returns only the expected matching entity.');
+    $target = array(
+      'value' => $entity_1->name->value . ' (1)',
+      'label' => check_plain($entity_1->name->value),
+    );
+    $this->assertIdentical(reset($data), $target, 'Autocomplete returns only the expected matching entity.');
 
     // Try to autocomplete a entity label that matches the second entity, and
     // the first entity  is already typed in the autocomplete (tags) widget.
     $input = $entity_1->name->value . ' (1), 10/17';
     $data = $this->getAutocompleteResult('tags', $input);
-    $this->assertIdentical($data[$entity_1->name->value . ' (1), ' . $entity_2->name->value . ' (2)'], check_plain($entity_2->name->value), 'Autocomplete returned the second matching entity');
+    $this->assertIdentical($data[0]['label'], check_plain($entity_2->name->value), 'Autocomplete returned the second matching entity');
 
     // Try to autocomplete a entity label with both a comma and a slash.
     $input = '"label with, and / t';
@@ -103,8 +106,11 @@ function testEntityReferenceAutocompletion() {
     if (strpos($entity_3->name->value, ',') !== FALSE || strpos($entity_3->name->value, '"') !== FALSE) {
       $n = '"' . str_replace('"', '""', $entity_3->name->value) .  ' (3)"';
     }
-    $target = array($n => check_plain($entity_3->name->value));
-    $this->assertIdentical($data, $target, 'Autocomplete returns an entity label containing a comma and a slash.');
+    $target = array(
+      'value' => $n,
+      'label' => check_plain($entity_3->name->value),
+    );
+    $this->assertIdentical(reset($data), $target, 'Autocomplete returns an entity label containing a comma and a slash.');
   }
 
   /**
diff --git a/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
index 090a27e8f384..2e69cde64672 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
@@ -131,7 +131,7 @@ public function testAuthorAutocomplete() {
 
     $this->drupalGet('node/add/page');
 
-    $result = $this->xpath('//input[@id = "edit-name-autocomplete"]');
+    $result = $this->xpath('//input[@id="edit-name" and contains(@data-autocomplete-path, "user/autocomplete")]');
     $this->assertEqual(count($result), 0, 'No autocompletion without access user profiles.');
 
     $admin_user = $this->drupalCreateUser(array('administer nodes', 'create page content', 'access user profiles'));
@@ -139,8 +139,7 @@ public function testAuthorAutocomplete() {
 
     $this->drupalGet('node/add/page');
 
-    $result = $this->xpath('//input[@id = "edit-name-autocomplete"]');
-    $this->assertEqual((string) $result[0]['value'], url('user/autocomplete'));
+    $result = $this->xpath('//input[@id="edit-name" and contains(@data-autocomplete-path, "user/autocomplete")]');
     $this->assertEqual(count($result), 1, 'Ensure that the user does have access to the autocompletion');
   }
 
diff --git a/core/modules/system/css/system.module.css b/core/modules/system/css/system.module.css
index eaa3b8569042..ef4cfae1ef9d 100644
--- a/core/modules/system/css/system.module.css
+++ b/core/modules/system/css/system.module.css
@@ -8,25 +8,6 @@
  *
  * @see autocomplete.js
  */
-/* Suggestion list */
-#autocomplete {
-  border: 1px solid;
-  overflow: hidden;
-  position: absolute;
-  z-index: 100;
-}
-#autocomplete ul {
-  list-style: none;
-  list-style-image: none;
-  margin: 0;
-  padding: 0;
-}
-#autocomplete li {
-  background: #fff;
-  color: #000;
-  cursor: default;
-  white-space: pre;
-}
 
 /* Animated throbber */
 .js input.form-autocomplete {
@@ -37,10 +18,10 @@
 .js[dir="rtl"] input.form-autocomplete {
   background-position: 0% 2px;
 }
-.js input.throbbing {
+.js input.form-autocomplete.ui-autocomplete-loading {
   background-position: 100% -18px; /* LTR */
 }
-.js[dir="rtl"] input.throbbing {
+.js[dir="rtl"] input.form-autocomplete.ui-autocomplete-loading {
   background-position: 0% -18px;
 }
 
diff --git a/core/modules/system/css/system.theme.css b/core/modules/system/css/system.theme.css
index bb9a347610d7..e5af9712f46d 100644
--- a/core/modules/system/css/system.theme.css
+++ b/core/modules/system/css/system.theme.css
@@ -192,9 +192,10 @@ label button.link {
  * @see autocomplete.js
  */
 /* Suggestion list */
-#autocomplete li.selected {
+.ui-autocomplete li.ui-menu-item a.ui-state-focus, .autocomplete li.ui-menu-item a.ui-state-hover {
   background: #0072b9;
   color: #fff;
+  margin: 0;
 }
 
 /**
diff --git a/core/modules/system/lib/Drupal/system/Tests/Form/ElementTest.php b/core/modules/system/lib/Drupal/system/Tests/Form/ElementTest.php
index 18c877095c6c..32120b2cabcb 100644
--- a/core/modules/system/lib/Drupal/system/Tests/Form/ElementTest.php
+++ b/core/modules/system/lib/Drupal/system/Tests/Form/ElementTest.php
@@ -133,20 +133,18 @@ function testGroupElements() {
   public function testFormAutocomplete() {
     $this->drupalGet('form-test/autocomplete');
 
-    $result = $this->xpath('//input[@id = "edit-autocomplete-1-autocomplete"]');
+    $result = $this->xpath('//input[@id="edit-autocomplete-1" and contains(@data-autocomplete-path, "form-test/autocomplete-1")]');
+    $this->assertEqual(count($result), 0, 'Ensure that the user does not have access to the autocompletion');
+    $result = $this->xpath('//input[@id="edit-autocomplete-2" and contains(@data-autocomplete-path, "form-test/autocomplete-2/value")]');
     $this->assertEqual(count($result), 0, 'Ensure that the user does not have access to the autocompletion');
-    $result = $this->xpath('//input[@id = "edit-autocomplete-2-autocomplete"]');
-    $this->assertEqual(count($result), 0, 'Ensure that the user did not had access to the autocompletion');
 
     $user = $this->drupalCreateUser(array('access autocomplete test'));
     $this->drupalLogin($user);
     $this->drupalGet('form-test/autocomplete');
 
-    $result = $this->xpath('//input[@id = "edit-autocomplete-1-autocomplete"]');
-    $this->assertEqual((string) $result[0]['value'], url('form-test/autocomplete-1'));
+    $result = $this->xpath('//input[@id="edit-autocomplete-1" and contains(@data-autocomplete-path, "form-test/autocomplete-1")]');
     $this->assertEqual(count($result), 1, 'Ensure that the user does have access to the autocompletion');
-    $result = $this->xpath('//input[@id = "edit-autocomplete-2-autocomplete"]');
-    $this->assertEqual((string) $result[0]['value'], url('form-test/autocomplete-2/value'));
+    $result = $this->xpath('//input[@id="edit-autocomplete-2" and contains(@data-autocomplete-path, "form-test/autocomplete-2/value")]');
     $this->assertEqual(count($result), 1, 'Ensure that the user does have access to the autocompletion');
   }
 
diff --git a/core/modules/system/system.module b/core/modules/system/system.module
index 11bef84991a4..78905dab0d02 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1096,7 +1096,9 @@ function system_library_info() {
     'dependencies' => array(
       array('system', 'jquery'),
       array('system', 'drupal'),
+      array('system', 'drupalSettings'),
       array('system', 'drupal.ajax'),
+      array('system', 'jquery.ui.autocomplete'),
     ),
   );
 
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php
index 616091e68d4d..1c1251664bd3 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Controller/TermAutocompleteController.php
@@ -200,7 +200,7 @@ protected function getMatchingTerms($tags_typed, array $vids, $tag_last) {
         if (strpos($name, ',') !== FALSE || strpos($name, '"') !== FALSE) {
           $name = '"' . str_replace('"', '""', $name) . '"';
         }
-        $matches[$prefix . $name] = String::checkPlain($term->label());
+        $matches[] = array('value' => $prefix . $name, 'label' => String::checkPlain($term->label()));
       }
       return $matches;
     }
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
index a468bed6f24e..dcae350b53f3 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/TermTest.php
@@ -220,13 +220,13 @@ function testNodeTermCreationAndDeletion() {
     // The term will be quoted, and the " will be encoded in unicode (\u0022).
     $input = substr($term_objects['term3']->label(), 0, 3);
     $json = $this->drupalGet('taxonomy/autocomplete/node/taxonomy_' . $this->vocabulary->id(), array('query' => array('q' => $input)));
-    $this->assertEqual($json, '{"\u0022' . $term_objects['term3']->label() . '\u0022":"' . $term_objects['term3']->label() . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term3']->label())));
+    $this->assertEqual($json, '[{"value":"\u0022' . $term_objects['term3']->label() . '\u0022","label":"' . $term_objects['term3']->label() . '"}]', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term3']->label())));
 
     // Test autocomplete on term 4 - it is alphanumeric only, so no extra
     // quoting.
     $input = substr($term_objects['term4']->label(), 0, 3);
     $this->drupalGet('taxonomy/autocomplete/node/taxonomy_' . $this->vocabulary->id(), array('query' => array('q' => $input)));
-    $this->assertRaw('{"' . $term_objects['term4']->label() . '":"' . $term_objects['term4']->label() . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term4']->label())));
+    $this->assertRaw('[{"value":"' . $term_objects['term4']->label() . '","label":"' . $term_objects['term4']->label() . '"}', format_string('Autocomplete returns term %term_name after typing the first 3 letters.', array('%term_name' => $term_objects['term4']->label())));
 
     // Test taxonomy autocomplete with a nonexistent field.
     $field_name = $this->randomName();
@@ -261,15 +261,18 @@ function testTermAutocompletion() {
     // The result order is not guaranteed, so check each term separately.
     $result = $this->drupalGet($path, array('query' => array('q' => $input)));
     $data = drupal_json_decode($result);
-    $this->assertEqual($data[$first_term->label()], check_plain($first_term->label()), 'Autocomplete returned the first matching term.');
-    $this->assertEqual($data[$second_term->label()], check_plain($second_term->label()), 'Autocomplete returned the second matching term.');
+    $this->assertEqual($data[0]['label'], check_plain($first_term->label()), 'Autocomplete returned the first matching term');
+    $this->assertEqual($data[1]['label'], check_plain($second_term->label()), 'Autocomplete returned the second matching term');
 
     // Try to autocomplete a term name that matches first term.
     // We should only get the first term in a json encoded string.
     $input = '10/16';
     $path = 'taxonomy/autocomplete/node/taxonomy_' . $this->vocabulary->id();
     $this->drupalGet($path, array('query' => array('q' => $input)));
-    $target = array($first_term->label() => check_plain($first_term->label()));
+    $target = array(array(
+      'value' => check_plain($first_term->label()),
+      'label' => $first_term->label(),
+    ));
     $this->assertRaw(drupal_json_encode($target), 'Autocomplete returns only the expected matching term.');
 
     // Try to autocomplete a term name with both a comma and a slash.
@@ -281,7 +284,10 @@ function testTermAutocompletion() {
     if (strpos($third_term->label(), ',') !== FALSE || strpos($third_term->label(), '"') !== FALSE) {
       $n = '"' . str_replace('"', '""', $third_term->label()) . '"';
     }
-    $target = array($n => check_plain($third_term->label()));
+    $target = array(array(
+      'value' => $n,
+      'label' => check_plain($third_term->label()),
+    ));
     $this->assertRaw(drupal_json_encode($target), 'Autocomplete returns a term containing a comma and a slash.');
   }
 
diff --git a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/Views/TaxonomyIndexTidUiTest.php b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/Views/TaxonomyIndexTidUiTest.php
index b95759d37b10..6b59ca618356 100644
--- a/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/Views/TaxonomyIndexTidUiTest.php
+++ b/core/modules/taxonomy/lib/Drupal/taxonomy/Tests/Views/TaxonomyIndexTidUiTest.php
@@ -101,8 +101,8 @@ public function testFilterUI() {
     $display['display_options']['filters']['tid']['type'] = 'textfield';
     $view->save();
     $this->drupalGet('admin/structure/views/nojs/config-item/test_filter_taxonomy_index_tid/default/filter/tid');
-    $result = $this->xpath('//input[@id = "edit-options-value-autocomplete"]');
-    $this->assertEqual((string) $result[0]['value'], url('taxonomy/autocomplete_vid/tags'));
+    $result = $this->xpath('//input[@id="edit-options-value"]/@data-autocomplete-path');
+    $this->assertEqual((string) $result[0], url('taxonomy/autocomplete_vid/tags'));
   }
 
 }
diff --git a/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php b/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php
index e6fad9a9ee3a..de8062e19c17 100644
--- a/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php
+++ b/core/modules/user/lib/Drupal/user/Tests/UserAutocompleteTest.php
@@ -53,8 +53,8 @@ function testUserAutocomplete() {
     // Test that anonymous username is in the result when requested and escaped
     // with check_plain().
     $users = $this->drupalGetJSON('user/autocomplete/anonymous', array('query' => array('q' => drupal_substr($anonymous_name, 0, 4))));
-    $this->assertTrue(in_array(check_plain($anonymous_name), $users), 'The anonymous name found in autocompletion results.');
+    $this->assertEqual(check_plain($anonymous_name), $users[0]['label'], 'The anonymous name found in autocompletion results.');
     $users = $this->drupalGetJSON('user/autocomplete', array('query' => array('q' => drupal_substr($anonymous_name, 0, 4))));
-    $this->assertFalse(isset($users[$anonymous_name]), 'The anonymous name not found in autocompletion results without enabling anonymous username.');
+    $this->assertTrue(empty($users), 'The anonymous name not found in autocompletion results without enabling anonymous username.');
   }
 }
diff --git a/core/modules/user/lib/Drupal/user/UserAutocomplete.php b/core/modules/user/lib/Drupal/user/UserAutocomplete.php
index 4d36923160a3..a7a20b29a227 100644
--- a/core/modules/user/lib/Drupal/user/UserAutocomplete.php
+++ b/core/modules/user/lib/Drupal/user/UserAutocomplete.php
@@ -7,6 +7,7 @@
 
 namespace Drupal\user;
 
+use Drupal\Component\Utility\String;
 use Drupal\Core\Config\ConfigFactory;
 use Drupal\Core\Database\Connection;
 
@@ -62,12 +63,12 @@ public function getMatches($string, $include_anonymous = FALSE) {
         $anonymous_name = $this->configFactory->get('user.settings')->get('anonymous');
         // Allow autocompletion for the anonymous user.
         if (stripos($anonymous_name, $string) !== FALSE) {
-          $matches[$anonymous_name] = check_plain($anonymous_name);
+          $matches[] = array('value' => $anonymous_name, 'label' => String::checkPlain($anonymous_name));
         }
       }
       $result = $this->connection->select('users')->fields('users', array('name'))->condition('name', db_like($string) . '%', 'LIKE')->range(0, 10)->execute();
       foreach ($result as $account) {
-        $matches[$account->name] = check_plain($account->name);
+        $matches[] = array('value' => $account->name, 'label' => String::checkPlain($account->name));
       }
     }
 
diff --git a/core/modules/views/lib/Drupal/views/Tests/ViewsTaxonomyAutocompleteTest.php b/core/modules/views/lib/Drupal/views/Tests/ViewsTaxonomyAutocompleteTest.php
index ef5f98dad2e3..df73f40220e9 100644
--- a/core/modules/views/lib/Drupal/views/Tests/ViewsTaxonomyAutocompleteTest.php
+++ b/core/modules/views/lib/Drupal/views/Tests/ViewsTaxonomyAutocompleteTest.php
@@ -8,7 +8,6 @@
 namespace Drupal\views\Tests;
 
 use Drupal\views\Tests\ViewTestBase;
-use Drupal\Component\Utility\MapArray;
 use Drupal\Core\Language\Language;
 
 /**
@@ -81,7 +80,10 @@ public function testTaxonomyAutocomplete() {
 
     // Test a with whole name term.
     $label = $this->term1->label();
-    $expected = MapArray::copyValuesToKeys((array) $label);
+    $expected = array(array(
+      'value' => $label,
+      'label' => check_plain($label),
+    ));
     $this->assertIdentical($expected, $this->drupalGetJSON($base_autocomplete_path, array('query' => array('q' => $label))));
     // Test a term by partial name.
     $partial = substr($label, 0, 2);
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index 6314e9310024..0768ddffd94a 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -1434,10 +1434,10 @@ input.form-submit:focus {
 .js[dir="rtl"] input.form-autocomplete {
   background-position: 1% 4px;
 }
-.js input.throbbing {
+.js input.form-autocomplete.ui-autocomplete-loading {
   background-position: 100% -16px; /* LTR */
 }
-.js[dir="rtl"] input.throbbing {
+.js[dir="rtl"] input.form-autocomplete.ui-autocomplete-loading {
   background-position: 1% -16px;
 }
 
diff --git a/core/themes/seven/style.css b/core/themes/seven/style.css
index e1f8f5ccdfe0..5700ea01ebd6 100644
--- a/core/themes/seven/style.css
+++ b/core/themes/seven/style.css
@@ -730,7 +730,6 @@ label {
 .form-item label.option input {
   vertical-align: middle;
 }
-.form-disabled input.form-autocomplete,
 .form-disabled input.form-text,
 .form-disabled input.form-tel,
 .form-disabled input.form-email,
-- 
GitLab