diff --git a/core/includes/form.inc b/core/includes/form.inc
index 8aee8f0..e77c95d 100644
--- a/core/includes/form.inc
+++ b/core/includes/form.inc
@@ -4116,20 +4116,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 Drupal.settings 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 0378b95..fd39feb 100644
--- a/core/misc/autocomplete.js
+++ b/core/misc/autocomplete.js
@@ -1,269 +1,183 @@
-(function ($) {
-
-"use strict";
+(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 i, character, quote = false;
+ var current = '';
+ var valueLength = value.length;
+
+ 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) {
+ // Get the desired term and construct the autocomplete URL for it.
+ var term = autocomplete.extractLastTerm(request.term);
+
+ /**
+ * Filter through the suggestions removing all terms already tagged and
+ * display the available terms to the user.
+ *
+ * @param {Array} 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) {
+ // Drupal returns an object, we need an array.
+ var terms = [];
+ for (var i in data) {
+ if (data.hasOwnProperty(i)) {
+ terms.push(data[i]);
+ }
+ }
+ autocomplete.cache[term] = terms;
+
+ // Send the new string array of terms to the jQuery UI list.
+ showSuggestions(terms);
+ }
+
+ // Check if the term is already cached.
+ if (autocomplete.cache.hasOwnProperty(term)) {
+ showSuggestions(autocomplete.cache[term]);
+ }
+ else {
+ // Construct the AJAX request from the provided defaults.
+ var ajax = Drupal.ajax;
+ var url = this.element.data('autocompletePath');
+ ajax.success = sourceCallbackHandler;
+ ajax.data = {q: term};
+ // Make the AJAX request.
+ $.ajax(url, ajax);
+ }
+}
+
+/**
+ * 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));
- }
- }
-};
-
-/**
- * Highlights the previous suggestion.
- */
-Drupal.jsAC.prototype.selectUp = function () {
- if (this.selected && this.selected.previousSibling) {
- this.highlight(this.selected.previousSibling);
- }
-};
-
-/**
- * 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());
-};
-
-/**
- * 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);
+ 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);
}
- }
-
- // 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();
+ },
+ 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.
+ * The autocomplete object, allowing overriding by contrib.
*/
Drupal.ACDB = function (uri) {
this.uri = uri;
@@ -274,65 +188,24 @@ Drupal.ACDB = function (uri) {
/**
* 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]);
+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/node/lib/Drupal/node/Tests/NodeCreationTest.php b/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
index 38e1c73..d3bfc39 100644
--- a/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
+++ b/core/modules/node/lib/Drupal/node/Tests/NodeCreationTest.php
@@ -132,7 +132,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'));
@@ -140,8 +140,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 71dd4c6..469624a 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 0b9af42..1c5ac89 100644
--- a/core/modules/system/css/system.theme.css
+++ b/core/modules/system/css/system.theme.css
@@ -189,9 +189,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 be13136..90157da 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-autocomplete"]');
+ $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 did not had access to the autocompletion');
+ $this->assertEqual(count($result), 0, 'Ensure that the user does not have 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 10bc48d..88f605a 100644
--- a/core/modules/system/system.module
+++ b/core/modules/system/system.module
@@ -1152,6 +1152,7 @@ function system_library_info() {
array('system', 'jquery'),
array('system', 'drupal'),
array('system', 'drupal.ajax'),
+ array('system', 'jquery.ui.autocomplete'),
),
);
diff --git a/core/themes/bartik/css/style.css b/core/themes/bartik/css/style.css
index dab43ff..aeec783 100644
--- a/core/themes/bartik/css/style.css
+++ b/core/themes/bartik/css/style.css
@@ -1416,10 +1416,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 c628c55..a2ddd5e 100644
--- a/core/themes/seven/style.css
+++ b/core/themes/seven/style.css
@@ -656,7 +656,6 @@ details summary {
.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,
@@ -760,7 +759,6 @@ body div.form-type-checkbox div.description {
text-shadow: none;
color: #999;
}
-input.form-autocomplete,
input.form-text,
input.form-tel,
input.form-email,
@@ -799,12 +797,6 @@ select.form-select:focus {
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1), 0 0 8px rgba(220, 220, 220, 0.4);
outline-color: rgba(0, 116, 189, 0.5);
}
-.js input.form-autocomplete {
- background-position: 100% 4px;
-}
-.js input.throbbing {
- background-position: 100% -16px;
-}
.button-action {
background: #1078d4;
background-image: -webkit-linear-gradient(top, #419ff1, #1076d5);