289 lines
7.5 KiB
JavaScript
289 lines
7.5 KiB
JavaScript
/**
|
|
* @file
|
|
* Autocomplete based on jQuery UI.
|
|
*/
|
|
|
|
(function($, Drupal) {
|
|
let autocomplete;
|
|
|
|
/**
|
|
* Helper splitting terms from the autocomplete value.
|
|
*
|
|
* @function Drupal.autocomplete.splitValues
|
|
*
|
|
* @param {string} value
|
|
* The value being entered by the user.
|
|
*
|
|
* @return {Array}
|
|
* Array of values, split by comma.
|
|
*/
|
|
function autocompleteSplitValues(value) {
|
|
// We will match the value against comma-separated terms.
|
|
const result = [];
|
|
let quote = false;
|
|
let current = '';
|
|
const valueLength = value.length;
|
|
let character;
|
|
|
|
for (let 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.
|
|
*
|
|
* @function Drupal.autocomplete.extractLastTerm
|
|
*
|
|
* @param {string} terms
|
|
* The value of the field.
|
|
*
|
|
* @return {string}
|
|
* The last value of the input field.
|
|
*/
|
|
function extractLastTerm(terms) {
|
|
return autocomplete.splitValues(terms).pop();
|
|
}
|
|
|
|
/**
|
|
* The search handler is called before a search is performed.
|
|
*
|
|
* @function Drupal.autocomplete.options.search
|
|
*
|
|
* @param {object} event
|
|
* The event triggered.
|
|
*
|
|
* @return {bool}
|
|
* Whether to perform a search or not.
|
|
*/
|
|
function searchHandler(event) {
|
|
const options = autocomplete.options;
|
|
|
|
if (options.isComposing) {
|
|
return false;
|
|
}
|
|
|
|
const term = autocomplete.extractLastTerm(event.target.value);
|
|
// Abort search if the first character is in firstCharacterBlacklist.
|
|
if (
|
|
term.length > 0 &&
|
|
options.firstCharacterBlacklist.indexOf(term[0]) !== -1
|
|
) {
|
|
return false;
|
|
}
|
|
// Only search when the term is at least the minimum length.
|
|
return term.length >= options.minLength;
|
|
}
|
|
|
|
/**
|
|
* JQuery UI autocomplete source callback.
|
|
*
|
|
* @param {object} request
|
|
* The request object.
|
|
* @param {function} response
|
|
* The function to call with the response.
|
|
*/
|
|
function sourceData(request, response) {
|
|
const 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
|
|
* Suggestions returned by the server.
|
|
*/
|
|
function showSuggestions(suggestions) {
|
|
const tagged = autocomplete.splitValues(request.term);
|
|
const il = tagged.length;
|
|
for (let i = 0; i < il; i++) {
|
|
const index = suggestions.indexOf(tagged[i]);
|
|
if (index >= 0) {
|
|
suggestions.splice(index, 1);
|
|
}
|
|
}
|
|
response(suggestions);
|
|
}
|
|
|
|
// Get the desired term and construct the autocomplete URL for it.
|
|
const term = autocomplete.extractLastTerm(request.term);
|
|
|
|
/**
|
|
* Transforms the data object into an array and update autocomplete results.
|
|
*
|
|
* @param {object} data
|
|
* The data sent back from the server.
|
|
*/
|
|
function sourceCallbackHandler(data) {
|
|
autocomplete.cache[elementId][term] = data;
|
|
|
|
// Send the new string array of terms to the jQuery UI list.
|
|
showSuggestions(data);
|
|
}
|
|
|
|
// Check if the term is already cached.
|
|
if (autocomplete.cache[elementId].hasOwnProperty(term)) {
|
|
showSuggestions(autocomplete.cache[elementId][term]);
|
|
} else {
|
|
const options = $.extend(
|
|
{ success: sourceCallbackHandler, data: { q: term } },
|
|
autocomplete.ajax,
|
|
);
|
|
$.ajax(this.element.attr('data-autocomplete-path'), options);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles an autocompletefocus event.
|
|
*
|
|
* @return {bool}
|
|
* Always returns false.
|
|
*/
|
|
function focusHandler() {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Handles an autocompleteselect event.
|
|
*
|
|
* @param {jQuery.Event} event
|
|
* The event triggered.
|
|
* @param {object} ui
|
|
* The jQuery UI settings object.
|
|
*
|
|
* @return {bool}
|
|
* Returns false to indicate the event status.
|
|
*/
|
|
function selectHandler(event, ui) {
|
|
const terms = autocomplete.splitValues(event.target.value);
|
|
// Remove the current input.
|
|
terms.pop();
|
|
// Add the selected item.
|
|
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;
|
|
}
|
|
|
|
/**
|
|
* Override jQuery UI _renderItem function to output HTML by default.
|
|
*
|
|
* @param {jQuery} ul
|
|
* jQuery collection of the ul element.
|
|
* @param {object} item
|
|
* The list item to append.
|
|
*
|
|
* @return {jQuery}
|
|
* jQuery collection of the ul element.
|
|
*/
|
|
function renderItem(ul, item) {
|
|
return $('<li>')
|
|
.append($('<a>').html(item.label))
|
|
.appendTo(ul);
|
|
}
|
|
|
|
/**
|
|
* Attaches the autocomplete behavior to all required fields.
|
|
*
|
|
* @type {Drupal~behavior}
|
|
*
|
|
* @prop {Drupal~behaviorAttach} attach
|
|
* Attaches the autocomplete behaviors.
|
|
* @prop {Drupal~behaviorDetach} detach
|
|
* Detaches the autocomplete behaviors.
|
|
*/
|
|
Drupal.behaviors.autocomplete = {
|
|
attach(context) {
|
|
// Act on textfields with the "form-autocomplete" class.
|
|
const $autocomplete = $(context)
|
|
.find('input.form-autocomplete')
|
|
.once('autocomplete');
|
|
if ($autocomplete.length) {
|
|
// Allow options to be overridden per instance.
|
|
const blacklist = $autocomplete.attr(
|
|
'data-autocomplete-first-character-blacklist',
|
|
);
|
|
$.extend(autocomplete.options, {
|
|
firstCharacterBlacklist: blacklist || '',
|
|
});
|
|
// Use jQuery UI Autocomplete on the textfield.
|
|
$autocomplete.autocomplete(autocomplete.options).each(function() {
|
|
$(this).data('ui-autocomplete')._renderItem =
|
|
autocomplete.options.renderItem;
|
|
});
|
|
|
|
// Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only.
|
|
$autocomplete.on('compositionstart.autocomplete', () => {
|
|
autocomplete.options.isComposing = true;
|
|
});
|
|
$autocomplete.on('compositionend.autocomplete', () => {
|
|
autocomplete.options.isComposing = false;
|
|
});
|
|
}
|
|
},
|
|
detach(context, settings, trigger) {
|
|
if (trigger === 'unload') {
|
|
$(context)
|
|
.find('input.form-autocomplete')
|
|
.removeOnce('autocomplete')
|
|
.autocomplete('destroy');
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Autocomplete object implementation.
|
|
*
|
|
* @namespace Drupal.autocomplete
|
|
*/
|
|
autocomplete = {
|
|
cache: {},
|
|
// Exposes options to allow overriding by contrib.
|
|
splitValues: autocompleteSplitValues,
|
|
extractLastTerm,
|
|
// jQuery UI autocomplete options.
|
|
|
|
/**
|
|
* JQuery UI option object.
|
|
*
|
|
* @name Drupal.autocomplete.options
|
|
*/
|
|
options: {
|
|
source: sourceData,
|
|
focus: focusHandler,
|
|
search: searchHandler,
|
|
select: selectHandler,
|
|
renderItem,
|
|
minLength: 1,
|
|
// Custom options, used by Drupal.autocomplete.
|
|
firstCharacterBlacklist: '',
|
|
// Custom options, indicate IME usage status.
|
|
isComposing: false,
|
|
},
|
|
ajax: {
|
|
dataType: 'json',
|
|
},
|
|
};
|
|
|
|
Drupal.autocomplete = autocomplete;
|
|
})(jQuery, Drupal);
|