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