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