You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
298 lines
8.4 KiB
298 lines
8.4 KiB
/** |
|
* @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(current.trim()); |
|
} |
|
|
|
return result; |
|
} |
|
|
|
/** |
|
* Returns the last value of a 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 {boolean} |
|
* 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 firstCharacterDenyList. |
|
if (term.length > 0 && options.firstCharacterDenyList.includes(term[0])) { |
|
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++) { |
|
if (suggestions.includes(tagged[i])) { |
|
suggestions.splice(suggestions.indexOf(tagged[i]), 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 autocomplete focus event. |
|
* |
|
* @return {boolean} |
|
* Always returns false. |
|
*/ |
|
function focusHandler() { |
|
return false; |
|
} |
|
|
|
/** |
|
* Handles an autocomplete select event. |
|
* |
|
* @param {jQuery.Event} event |
|
* The event triggered. |
|
* @param {object} ui |
|
* The jQuery UI settings object. |
|
* |
|
* @return {boolean} |
|
* 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. |
|
once('autocomplete', 'input.form-autocomplete', context).forEach( |
|
(element) => { |
|
const $autocomplete = $(element); |
|
// cspell:ignore blacklist |
|
// Allow options to be overridden per instance. |
|
const blacklist = $autocomplete.attr( |
|
'data-autocomplete-first-character-blacklist', |
|
); |
|
if (blacklist !== undefined) { |
|
Drupal.deprecationError({ |
|
message: |
|
'The data-autocomplete-first-character-blocklist attribute is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use firstCharacterDenyList instead. See https://www.drupal.org/node/3472016.', |
|
}); |
|
} |
|
const denyList = $autocomplete.attr( |
|
'data-autocomplete-first-character-denylist', |
|
); |
|
Drupal.deprecatedProperty({ |
|
target: autocomplete.options, |
|
deprecatedProperty: 'firstCharacterBlacklist', |
|
message: |
|
'The firstCharacterBlacklist property is deprecated in drupal:11.2.0 and is removed from drupal:12.0.0. Use firstCharacterDenyList instead. See https://www.drupal.org/node/3472016.', |
|
}); |
|
$.extend(autocomplete.options, { |
|
firstCharacterDenyList: denyList || 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') { |
|
$( |
|
once.remove('autocomplete', 'input.form-autocomplete', context), |
|
).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. |
|
firstCharacterDenyList: '', |
|
// Custom options, indicate IME usage status. |
|
isComposing: false, |
|
}, |
|
ajax: { |
|
dataType: 'json', |
|
jsonp: false, |
|
}, |
|
}; |
|
|
|
Drupal.autocomplete = autocomplete; |
|
})(jQuery, Drupal);
|
|
|