--- /dev/null
+/**
+ * @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);
+ }
+
+ /**
+ * 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);
+ }
+
+ // 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 {
+ 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));