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(current.trim()); 42 } 43 44 return result; 45 } 46 47 /** 48 * Returns the last value of a 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 {boolean} 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 {boolean} 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 {boolean} 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>').append($('<a>').html(item.label)).appendTo(ul); 200 } 201 202 /** 203 * Attaches the autocomplete behavior to all required fields. 204 * 205 * @type {Drupal~behavior} 206 * 207 * @prop {Drupal~behaviorAttach} attach 208 * Attaches the autocomplete behaviors. 209 * @prop {Drupal~behaviorDetach} detach 210 * Detaches the autocomplete behaviors. 211 */ 212 Drupal.behaviors.autocomplete = { 213 attach(context) { 214 // Act on textfields with the "form-autocomplete" class. 215 once('autocomplete', 'input.form-autocomplete', context).forEach( 216 (element) => { 217 const $autocomplete = $(element); 218 // Allow options to be overridden per instance. 219 const blacklist = $autocomplete.attr( 220 'data-autocomplete-first-character-blacklist', 221 ); 222 $.extend(autocomplete.options, { 223 firstCharacterBlacklist: blacklist || '', 224 }); 225 // Use jQuery UI Autocomplete on the textfield. 226 $autocomplete.autocomplete(autocomplete.options).each(function () { 227 $(this).data('ui-autocomplete')._renderItem = 228 autocomplete.options.renderItem; 229 }); 230 231 // Use CompositionEvent to handle IME inputs. It requests remote server on "compositionend" event only. 232 $autocomplete.on('compositionstart.autocomplete', () => { 233 autocomplete.options.isComposing = true; 234 }); 235 $autocomplete.on('compositionend.autocomplete', () => { 236 autocomplete.options.isComposing = false; 237 }); 238 }, 239 ); 240 }, 241 detach(context, settings, trigger) { 242 if (trigger === 'unload') { 243 $( 244 once.remove('autocomplete', 'input.form-autocomplete', context), 245 ).autocomplete('destroy'); 246 } 247 }, 248 }; 249 250 /** 251 * Autocomplete object implementation. 252 * 253 * @namespace Drupal.autocomplete 254 */ 255 autocomplete = { 256 cache: {}, 257 // Exposes options to allow overriding by contrib. 258 splitValues: autocompleteSplitValues, 259 extractLastTerm, 260 // jQuery UI autocomplete options. 261 262 /** 263 * JQuery UI option object. 264 * 265 * @name Drupal.autocomplete.options 266 */ 267 options: { 268 source: sourceData, 269 focus: focusHandler, 270 search: searchHandler, 271 select: selectHandler, 272 renderItem, 273 minLength: 1, 274 // Custom options, used by Drupal.autocomplete. 275 firstCharacterBlacklist: '', 276 // Custom options, indicate IME usage status. 277 isComposing: false, 278 }, 279 ajax: { 280 dataType: 'json', 281 jsonp: false, 282 }, 283 }; 284 285 Drupal.autocomplete = autocomplete; 286})(jQuery, Drupal);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.