123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288 |
- /**
- * @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 overriden 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);
|