Security update for Core, with self-updated composer
[yaffs-website] / web / core / misc / autocomplete.es6.js
diff --git a/web/core/misc/autocomplete.es6.js b/web/core/misc/autocomplete.es6.js
new file mode 100644 (file)
index 0000000..eeac77e
--- /dev/null
@@ -0,0 +1,280 @@
+/**
+ * @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));