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.
parent
fcea2f9e3f
commit
02a10b3161
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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($('<span class="visually-hidden" aria-live="assertive"></span>')
|
||||
.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 = $('<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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 = $('<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);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
||||
|
|
|
@ -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'),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue