Security update to Drupal 8.4.6
[yaffs-website] / web / core / misc / autocomplete.es6.js
1 /**
2  * @file
3  * Autocomplete based on jQuery UI.
4  */
5
6 (function ($, Drupal) {
7   let autocomplete;
8
9   /**
10    * Helper splitting terms from the autocomplete value.
11    *
12    * @function Drupal.autocomplete.splitValues
13    *
14    * @param {string} value
15    *   The value being entered by the user.
16    *
17    * @return {Array}
18    *   Array of values, split by comma.
19    */
20   function autocompleteSplitValues(value) {
21     // We will match the value against comma-separated terms.
22     const result = [];
23     let quote = false;
24     let current = '';
25     const valueLength = value.length;
26     let character;
27
28     for (let i = 0; i < valueLength; i++) {
29       character = value.charAt(i);
30       if (character === '"') {
31         current += character;
32         quote = !quote;
33       }
34       else if (character === ',' && !quote) {
35         result.push(current.trim());
36         current = '';
37       }
38       else {
39         current += character;
40       }
41     }
42     if (value.length > 0) {
43       result.push($.trim(current));
44     }
45
46     return result;
47   }
48
49   /**
50    * Returns the last value of an multi-value textfield.
51    *
52    * @function Drupal.autocomplete.extractLastTerm
53    *
54    * @param {string} terms
55    *   The value of the field.
56    *
57    * @return {string}
58    *   The last value of the input field.
59    */
60   function extractLastTerm(terms) {
61     return autocomplete.splitValues(terms).pop();
62   }
63
64   /**
65    * The search handler is called before a search is performed.
66    *
67    * @function Drupal.autocomplete.options.search
68    *
69    * @param {object} event
70    *   The event triggered.
71    *
72    * @return {bool}
73    *   Whether to perform a search or not.
74    */
75   function searchHandler(event) {
76     const options = autocomplete.options;
77
78     if (options.isComposing) {
79       return false;
80     }
81
82     const term = autocomplete.extractLastTerm(event.target.value);
83     // Abort search if the first character is in firstCharacterBlacklist.
84     if (term.length > 0 && options.firstCharacterBlacklist.indexOf(term[0]) !== -1) {
85       return false;
86     }
87     // Only search when the term is at least the minimum length.
88     return term.length >= options.minLength;
89   }
90
91   /**
92    * JQuery UI autocomplete source callback.
93    *
94    * @param {object} request
95    *   The request object.
96    * @param {function} response
97    *   The function to call with the response.
98    */
99   function sourceData(request, response) {
100     const elementId = this.element.attr('id');
101
102     if (!(elementId in autocomplete.cache)) {
103       autocomplete.cache[elementId] = {};
104     }
105
106     /**
107      * Filter through the suggestions removing all terms already tagged and
108      * display the available terms to the user.
109      *
110      * @param {object} suggestions
111      *   Suggestions returned by the server.
112      */
113     function showSuggestions(suggestions) {
114       const tagged = autocomplete.splitValues(request.term);
115       const il = tagged.length;
116       for (let i = 0; i < il; i++) {
117         const index = suggestions.indexOf(tagged[i]);
118         if (index >= 0) {
119           suggestions.splice(index, 1);
120         }
121       }
122       response(suggestions);
123     }
124
125     /**
126      * Transforms the data object into an array and update autocomplete results.
127      *
128      * @param {object} data
129      *   The data sent back from the server.
130      */
131     function sourceCallbackHandler(data) {
132       autocomplete.cache[elementId][term] = data;
133
134       // Send the new string array of terms to the jQuery UI list.
135       showSuggestions(data);
136     }
137
138     // Get the desired term and construct the autocomplete URL for it.
139     var term = autocomplete.extractLastTerm(request.term);
140
141     // Check if the term is already cached.
142     if (autocomplete.cache[elementId].hasOwnProperty(term)) {
143       showSuggestions(autocomplete.cache[elementId][term]);
144     }
145     else {
146       const options = $.extend({ success: sourceCallbackHandler, data: { q: term } }, autocomplete.ajax);
147       $.ajax(this.element.attr('data-autocomplete-path'), options);
148     }
149   }
150
151   /**
152    * Handles an autocompletefocus event.
153    *
154    * @return {bool}
155    *   Always returns false.
156    */
157   function focusHandler() {
158     return false;
159   }
160
161   /**
162    * Handles an autocompleteselect event.
163    *
164    * @param {jQuery.Event} event
165    *   The event triggered.
166    * @param {object} ui
167    *   The jQuery UI settings object.
168    *
169    * @return {bool}
170    *   Returns false to indicate the event status.
171    */
172   function selectHandler(event, ui) {
173     const terms = autocomplete.splitValues(event.target.value);
174     // Remove the current input.
175     terms.pop();
176     // Add the selected item.
177     terms.push(ui.item.value);
178
179     event.target.value = terms.join(', ');
180     // Return false to tell jQuery UI that we've filled in the value already.
181     return false;
182   }
183
184   /**
185    * Override jQuery UI _renderItem function to output HTML by default.
186    *
187    * @param {jQuery} ul
188    *   jQuery collection of the ul element.
189    * @param {object} item
190    *   The list item to append.
191    *
192    * @return {jQuery}
193    *   jQuery collection of the ul element.
194    */
195   function renderItem(ul, item) {
196     return $('<li>')
197       .append($('<a>').html(item.label))
198       .appendTo(ul);
199   }
200
201   /**
202    * Attaches the autocomplete behavior to all required fields.
203    *
204    * @type {Drupal~behavior}
205    *
206    * @prop {Drupal~behaviorAttach} attach
207    *   Attaches the autocomplete behaviors.
208    * @prop {Drupal~behaviorDetach} detach
209    *   Detaches the autocomplete behaviors.
210    */
211   Drupal.behaviors.autocomplete = {
212     attach(context) {
213       // Act on textfields with the "form-autocomplete" class.
214       const $autocomplete = $(context).find('input.form-autocomplete').once('autocomplete');
215       if ($autocomplete.length) {
216         // Allow options to be overriden per instance.
217         const blacklist = $autocomplete.attr('data-autocomplete-first-character-blacklist');
218         $.extend(autocomplete.options, {
219           firstCharacterBlacklist: (blacklist) || '',
220         });
221         // Use jQuery UI Autocomplete on the textfield.
222         $autocomplete.autocomplete(autocomplete.options)
223           .each(function () {
224             $(this).data('ui-autocomplete')._renderItem = autocomplete.options.renderItem;
225           });
226
227         // Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only.
228         $autocomplete.on('compositionstart.autocomplete', () => {
229           autocomplete.options.isComposing = true;
230         });
231         $autocomplete.on('compositionend.autocomplete', () => {
232           autocomplete.options.isComposing = false;
233         });
234       }
235     },
236     detach(context, settings, trigger) {
237       if (trigger === 'unload') {
238         $(context).find('input.form-autocomplete')
239           .removeOnce('autocomplete')
240           .autocomplete('destroy');
241       }
242     },
243   };
244
245   /**
246    * Autocomplete object implementation.
247    *
248    * @namespace Drupal.autocomplete
249    */
250   autocomplete = {
251     cache: {},
252     // Exposes options to allow overriding by contrib.
253     splitValues: autocompleteSplitValues,
254     extractLastTerm,
255     // jQuery UI autocomplete options.
256
257     /**
258      * JQuery UI option object.
259      *
260      * @name Drupal.autocomplete.options
261      */
262     options: {
263       source: sourceData,
264       focus: focusHandler,
265       search: searchHandler,
266       select: selectHandler,
267       renderItem,
268       minLength: 1,
269       // Custom options, used by Drupal.autocomplete.
270       firstCharacterBlacklist: '',
271       // Custom options, indicate IME usage status.
272       isComposing: false,
273     },
274     ajax: {
275       dataType: 'json',
276     },
277   };
278
279   Drupal.autocomplete = autocomplete;
280 }(jQuery, Drupal));