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