From 02a10b31610fa1ef576057947e5c275f517fe908 Mon Sep 17 00:00:00 2001 From: webchick 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 | 504 +++++++----------- .../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, 238 insertions(+), 393 deletions(-) diff --git a/core/includes/form.inc b/core/includes/form.inc index c82124a4f6c..d12a9b948d2 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 0378b9525d6..ad9bb1cdf84 100644 --- a/core/misc/autocomplete.js +++ b/core/misc/autocomplete.js @@ -1,338 +1,200 @@ -(function ($) { +(function ($, Drupal) { "use strict"; +var autocomplete; + +/** + * Helper splitting terms from the autocomplete value. + * + * @param {String} value + * + * @return {Array} + */ +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; + + 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; + } + } + if (value.length > 0) { + result.push($.trim(current)); + } + + return result; +} + +/** + * Returns the last value of an multi-value textfield. + * + * @param {String} terms + * + * @return {String} + */ +function extractLastTerm (terms) { + return autocomplete.splitValues(terms).pop(); +} + +/** + * The search handler is called before a search is performed. + * + * @param {Object} event + * + * @return {Boolean} + */ +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; +} + +/** + * jQuery UI autocomplete source callback. + * + * @param {Object} request + * @param {Function} response + */ +function sourceData (request, response) { + var elementId = this.element.attr('id'); + + if (!(elementId in autocomplete.cache)) { + autocomplete.cache[elementId] = {}; + } + + /** + * 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); + } + + /** + * Transforms the data object into an array and update autocomplete results. + * + * @param {Object} data + */ + function sourceCallbackHandler (data) { + autocomplete.cache[elementId][term] = data; + + // Send the new string array of terms to the jQuery UI list. + showSuggestions(data); + } + + // Get the desired term and construct the autocomplete URL for it. + var term = autocomplete.extractLastTerm(request.term); + + // Check if the term is already cached. + if (autocomplete.cache[elementId].hasOwnProperty(term)) { + showSuggestions(autocomplete.cache[elementId][term]); + } + else { + var options = $.extend({ success: sourceCallbackHandler, data: { q: term } }, autocomplete.ajax); + /*jshint validthis:true */ + $.ajax(this.element.attr('data-autocomplete-path'), options); + } +} + +/** + * Handles an autocompletefocus event. + * + * @return {Boolean} + */ +function focusHandler () { + return false; +} + +/** + * Handles an autocompleteselect event. + * + * @param {Object} event + * @param {Object} ui + * + * @return {Boolean} + */ +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 + '"'); + } + 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; +} + /** * 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($('') - .attr('id', $input[0].id + '-autocomplete-aria-live') - ); - new Drupal.jsAC($input, acdb[uri]); - }); - } -}; - -/** - * Prevents the form from submitting if the suggestions popup is open - * and closes the suggestions popup when doing so. - */ -Drupal.autocompleteSubmit = function () { - var $autocomplete = $('#autocomplete'); - if ($autocomplete.length !== 0) { - $autocomplete[0].owner.hidePopup(); - } - return $autocomplete.length === 0; -}; - -/** - * 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; - } - 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; - } -}; - -/** - * Handler for the "keyup" event. - */ -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; - } -}; - -/** - * Puts the currently highlighted suggestion into the autocomplete field. - */ -Drupal.jsAC.prototype.select = function (node) { - this.input.value = $(node).data('autocompleteValue'); -}; - -/** - * Highlights the next suggestion. - */ -Drupal.jsAC.prototype.selectDown = function () { - if (this.selected && this.selected.nextSibling) { - this.highlight(this.selected.nextSibling); - } - else if (this.popup) { - var lis = $(this.popup).find('li'); - if (lis.length > 0) { - this.highlight(lis.get(0)); + 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); + } + }, + detach: function (context, settings, trigger) { + if (trigger === 'unload') { + $(context).find('input.form-autocomplete') + .removeOnce('autocomplete') + .autocomplete('destroy'); } } }; /** - * Highlights the previous suggestion. + * Autocomplete object implementation. */ -Drupal.jsAC.prototype.selectUp = function () { - if (this.selected && this.selected.previousSibling) { - this.highlight(this.selected.previousSibling); +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' } }; -/** - * 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'); - } - $(node).addClass('selected'); - this.selected = node; - $(this.ariaLive).html($(this.selected).html()); -}; +Drupal.autocomplete = autocomplete; -/** - * Unhighlights a suggestion. - */ -Drupal.jsAC.prototype.unhighlight = function (node) { - $(node).removeClass('selected'); - this.selected = false; - $(this.ariaLive).empty(); -}; - -/** - * 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'); - } - // Hide popup. - var popup = this.popup; - if (popup) { - this.popup = null; - $(popup).fadeOut('fast', function () { $(popup).remove(); }); - } - this.selected = false; - $(this.ariaLive).empty(); -}; - -/** - * Positions the suggestions popup and starts a search. - */ -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 = $('
')[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); -}; - -/** - * Fills the suggestion popup with any matches received. - */ -Drupal.jsAC.prototype.found = function (matches) { - // If no value in the textfield, do not show the popup. - if (!this.input.value.length) { - return false; - } - - // Prepare matches. - var ac = this; - var 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)) { - $('
  • ') - .html($('
    ').html(matches[key])) - .data('autocompleteValue', key) - .appendTo(ul); - } - } - - // 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')); - } - else { - $(this.popup).css({ visibility: 'hidden' }); - this.hidePopup(); - } - } -}; - -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. - */ -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]); - } - - // 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 = ''; -}; - -})(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 56ce263635f..96909a90530 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 @@ class EntityReferenceAutocomplete { 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 45844ceecea..98953685ca9 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 @@ class EntityReferenceAutocompleteTest extends EntityUnitTestBase { // 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 @@ class EntityReferenceAutocompleteTest extends EntityUnitTestBase { 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 090a27e8f38..2e69cde6467 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 @@ class NodeCreationTest extends NodeTestBase { $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 @@ class NodeCreationTest extends NodeTestBase { $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 eaa3b856904..ef4cfae1ef9 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 bb9a347610d..e5af9712f46 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 18c877095c6..32120b2cabc 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 @@ class ElementTest extends WebTestBase { 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 11bef84991a..78905dab0d0 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 616091e68d4..1c1251664bd 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 @@ class TermAutocompleteController implements ContainerInjectionInterface { 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 a468bed6f24..dcae350b53f 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 @@ class TermTest extends TaxonomyTestBase { // 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 @@ class TermTest extends TaxonomyTestBase { // 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 @@ class TermTest extends TaxonomyTestBase { 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 b95759d37b1..6b59ca61835 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 @@ class TaxonomyIndexTidUiTest extends UITestBase { $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 e6fad9a9ee3..de8062e19c1 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 @@ class UserAutocompleteTest extends WebTestBase { // 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 4d36923160a..a7a20b29a22 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 @@ class UserAutocomplete { $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 ef5f98dad2e..df73f40220e 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 @@ class ViewsTaxonomyAutocompleteTest extends ViewTestBase { // 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 6314e931002..0768ddffd94 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 e1f8f5ccdfe..5700ea01ebd 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,