https://www.hymer-steigtechnik.de/_assets/e799cca3073eabb617dfebf5…/suggest_controller.js?1784301410
1class SuggestController { 2 constructor(element) { 3 this.form = element; 4 this.suggestURL = this.form.getAttribute('data-suggest') 5 this.documentSuggestionHeader = this.form.getAttribute('data-suggest-header') 6 this.suggestionItems = [] 7 this.documentItems = [] 8 this.lastSelectedIndex = null 9 this.isMouseNavigation = false 10 this.autoCompleteInstance = null 11 } 12 13 /** 14 * Initialize autocomplete/suggestions 15 */ 16 init() { 17 this.buildSuggestionBox(); 18 this.setSuggestionBoxWidthAndPosition(); 19 } 20 21 /** 22 * Reset autocomplete and document suggestions 23 */ 24 resetSuggestions() { 25 this.suggestionItems = [] 26 this.documentItems = [] 27 } 28 29 /** 30 * Format search query. Avoid XSS 31 * @param query 32 * @returns {string} 33 */ 34 formatQuery(query) { 35 if (typeof query !== "string") { 36 return ""; 37 } 38 // Escape HTML characters (XSS-Protection) 39 const htmlEscaped = query 40 .replace(/&/g, "&") 41 .replace(/</g, "<") 42 .replace(/>/g, ">") 43 .replace(/"/g, """) 44 .replace(/'/g, "'") 45 .replace(/\//g, "/"); 46 // Only keep letters, numbers, spaces, hyphens, underscore, period, comma 47 const cleaned = htmlEscaped.replace(/[^\p{L}\p{N}\s\-_.,]/gu, ""); 48 49 // Remove multiple spaces 50 return cleaned.trim().replace(/\s+/g, " "); 51 } 52 53 /** 54 * Format result to avoid display errors 55 * @param result 56 * @returns {string} 57 */ 58 formatResult(result) { 59 if (typeof result !== "string") { 60 return ""; 61 } 62 // Escape HTML characters. Only allow <strong> 63 const htmlEscaped = result 64 .replace(/&/g, "&") 65 .replace(/</g, "<") 66 .replace(/>/g, ">") 67 .replace(/"/g, """) 68 .replace(/'/g, "'") 69 .replace(/<\/?mark>/g, (match) => match.includes('/') ? '</strong>' : '<strong>'); 70 71 return htmlEscaped; 72 } 73 74 /** 75 * Fetch autocomplete and document suggestions 76 * @param query 77 * @returns {Promise<[]|*|*[]>} 78 */ 79 async fetchSuggestions(query) { 80 // Cancel request if there is one 81 if (this.abortController) { 82 this.abortController.abort(); 83 } 84 this.resetSuggestions() 85 // Create AbortController to avoid multiple requests 86 this.abortController = new AbortController(); 87 88 try { 89 const separator = this.suggestURL.includes('?') ? '&' : '?'; 90 const url = `${this.suggestURL}${separator}tx_solr%5BqueryString%5D=${encodeURIComponent(query)}`; 91 const response = await fetch( 92 url, 93 { signal: this.abortController.signal } 94 ); 95 const data = await response.json(); 96 // Define autocomplete and document suggestions 97 this.suggestionItems = data.suggestions ? Object.keys(data.suggestions) : []; 98 this.documentItems = Array.isArray(data.documents) ? data.documents : []; 99 100 return this.suggestionItems; 101 } catch (error) { 102 if (error.name === 'AbortError') { 103 // Aborted request 104 return []; 105 } 106 // Real error 107 throw error; 108 } 109 } 110 111 /** 112 * Set input value to initial query, set cursor behind search term, remove active-state from items 113 */ 114 resetInput() { 115 const list = this.autoCompleteInstance.list; 116 const selected = list.querySelector(`.${this.autoCompleteInstance.resultItem.selected}`); 117 118 // Remove selected state 119 if (selected) { 120 selected.removeAttribute("aria-selected"); 121 selected.classList.remove(this.autoCompleteInstance.resultItem.selected); 122 } 123 124 this.autoCompleteInstance.cursor = -1 125 // Set input value to initial query 126 this.autoCompleteInstance.input.value = this.autoCompleteInstance.feedback.query; 127 // Set cursor behind the search term 128 this.setCursorBehindTerm() 129 } 130 131 /** 132 * Set cursor at the end of the search term 133 */ 134 setCursorBehindTerm() { 135 const queryLength = this.autoCompleteInstance.input.value.length 136 137 setTimeout(() => { 138 this.autoCompleteInstance.input.setSelectionRange(queryLength, queryLength) 139 }, 0); 140 } 141 142 /** 143 * Handler for arrow up key 144 */ 145 arrowUpHandler() { 146 if (this.autoCompleteInstance.isOpen && this.lastSelectedIndex !== null) { 147 // If first result item is selected and arrow up is pressed then 148 // replace input value with initial query and reset suggestion box. 149 if (this.lastSelectedIndex === 0) { 150 this.resetInput() 151 } else { 152 this.autoCompleteInstance.previous() 153 this.setCursorBehindTerm() 154 } 155 } 156 } 157 158 /** 159 * Handler for arrow down key 160 */ 161 arrowDownHandler() { 162 const list = this.autoCompleteInstance.list; 163 164 if (this.autoCompleteInstance.isOpen && list) { 165 const items = list.querySelectorAll("li"); 166 167 // Navigate to next item as long as it's not last item 168 if (this.lastSelectedIndex < items.length - 1) { 169 this.autoCompleteInstance.next() 170 } 171 } else if (list) { 172 // Open suggestion box on arrow down key 173 this.autoCompleteInstance.open() 174 } 175 } 176 177 /** 178 * Handler for enter key 179 */ 180 enterHandler() { 181 const cursor = this.autoCompleteInstance.cursor 182 // If nothing is selected or suggestion box is closed then submit form 183 if ((cursor === undefined || cursor === -1) || !this.autoCompleteInstance.isOpen) { 184 if (this.autoCompleteInstance.input.value.length) { 185 this.form.requestSubmit() 186 } 187 } else { 188 this.autoCompleteInstance.select(cursor) 189 } 190 } 191 192 /** 193 * Handler for escape key 194 */ 195 escapeHandler() { 196 if (this.autoCompleteInstance.isOpen) { 197 this.autoCompleteInstance.close() 198 this.resetInput() 199 } 200 } 201 202 /** 203 * Create document suggestion item under "Top Results" 204 * @param documentItem 205 * @returns {HTMLLIElement} 206 */ 207 createDocumentSuggestionItem(documentItem) { 208 const documentElement = document.createElement("li"); 209 const formattedDocument = this.formatResult(documentItem.title) 210 const link = document.createElement("a"); 211 const index = this.autoCompleteInstance.list.querySelectorAll('li').length; 212 213 link.href = documentItem.link; 214 link.innerHTML = formattedDocument; 215 documentElement.appendChild(link); 216 documentElement.classList.add('autocomplete-suggestion') 217 this.onResultItemHover(documentElement, index) 218 219 return documentElement 220 } 221 222 /** 223 * Create document suggestion header (Top results) 224 * @returns {HTMLParagraphElement} 225 */ 226 createDocumentSuggestionHeader() { 227 const documentHeader = document.createElement("p"); 228 229 documentHeader.classList.add('autocomplete-group') 230 documentHeader.innerHTML = `<span>${this.documentSuggestionHeader}</span>`;
231 232 return documentHeader 233 } 234 235 /** 236 * Set hovered result items as selected 237 * @param item 238 * @param index 239 */ 240 onResultItemHover(item, index) { 241 item.onmouseenter = () => { 242 this.lastSelectedIndex = index 243 // Set navigateVia to mouse to prevent input value replacement via hover later on. Only via keyboard. 244 this.isMouseNavigation = true 245 // Set hovered autocomplete suggestion as selected 246 this.autoCompleteInstance.goTo(index) 247 } 248 } 249 250 /** 251 * Set suggestion box position and width 252 */ 253 setSuggestionBoxWidthAndPosition() { 254 const list = this.autoCompleteInstance.list; 255 if (!list) return; 256 257 // Move suggestion box to end of body tag to prevent problems with overflow-hidden parents 258 document.body.appendChild(list); 259 260 const input = this.autoCompleteInstance.input; 261 const inputRect = input.getBoundingClientRect(); 262 263 list.style.top = `${inputRect.bottom + window.scrollY}px`; 264 list.style.left = `${inputRect.left + window.scrollX}px`; 265 list.style.width = `${inputRect.width}px`; 266 } 267 268 /** 269 * Build suggestion box with autoCompleteJS 270 * https://tarekraafat.github.io/autoComplete.js/#/configuration 271 */ 272 buildSuggestionBox() { 273 if (this.autoCompleteInstance) { 274 this.autoCompleteInstance.unInit(); 275 } 276 this.autoCompleteInstance = new autoComplete( 277 { 278 selector: ".tx-solr-suggest", 279 data: { 280 src: async (query) => { 281 const formattedQuery = this.formatQuery(query) 282 return await this.fetchSuggestions(formattedQuery); 283 }, 284 cache: false, 285 }, 286 debounce: 300, 287 wrapper: false, 288 resultsList: { 289 class: 'autocomplete-suggestions tx-solr-autosuggest', 290 element: (list) => { 291 // Add document suggestions (link) to result list under "Top results" 292 if (this.documentItems.length) { 293 list.append(this.createDocumentSuggestionHeader()); 294 this.documentItems.forEach((documentItem) => { 295 list.append(this.createDocumentSuggestionItem(documentItem)); 296 }) 297 } 298 } 299 }, 300 resultItem: { 301 highlight: true, 302 class: 'autocomplete-suggestion', 303 selected: 'autocomplete-selected', 304 element: (item, data) => { 305 item.innerHTML = `${this.formatResult(data.match)}`; 306 // Set hovered autocomplete suggestion as selected 307 const index = this.suggestionItems.indexOf(data.value); 308 this.onResultItemHover(item, index) 309 }, 310 }, 311 events: { 312 input: { 313 focus: () => { 314 const inputValueLength = this.autoCompleteInstance.input.value.length 315 const resultItemsLength = this.autoCompleteInstance.list.children.length 316 // Reopen suggestion list when results are still the same. Otherwise, start new request 317 inputValueLength && resultItemsLength ? this.autoCompleteInstance.open() : this.autoCompleteInstance.start(); 318 319 }, 320 selection: (event) => { 321 if (!this.autoCompleteInstance.isOpen) return; 322 const selectionIndex = event.detail.selection.index 323 324 this.autoCompleteInstance.input.blur(); 325 // Replace input value with the selected value 326 this.autoCompleteInstance.input.value = this.formatQuery(event.detail.selection.value); 327 328 // If selected element comes after the autocomplete suggestions (document suggestion) 329 if (selectionIndex >= event.detail.results.length) { 330 event.preventDefault(); 331 const resultItems = this.autoCompleteInstance.list.querySelectorAll('li') 332 333 // Get href from selected item and open it 334 window.location.href = resultItems[selectionIndex].children[0].href 335 } else { 336 this.form.requestSubmit() 337 } 338 }, 339 navigate: (event) => { 340 if (!this.autoCompleteInstance.isOpen) return; 341 if (!this.isMouseNavigation) { 342 // Replace value of focused result item with input field value (only keyboard) 343 if (event.detail.selection.value) { 344 this.autoCompleteInstance.input.value = event.detail.selection.value; 345 } 346 } 347 this.isMouseNavigation = false 348 }, 349 open: () => { 350 this.lastSelectedIndex = null 351 this.setSuggestionBoxWidthAndPosition(); 352 }, 353 close: () => { 354 this.lastSelectedIndex = null 355 // Abort all running requests 356 this.abortController.abort(); 357 }, 358 keydown: (event) => { 359 const cursor = this.autoCompleteInstance.cursor; 360 this.lastSelectedIndex = (cursor !== undefined && cursor >= 0) ? cursor : null; 361 362 switch (event.key) { 363 case 'ArrowDown': 364 this.arrowDownHandler(); 365 break; 366 case 'ArrowUp': 367 this.arrowUpHandler(); 368 break; 369 case 'Enter': 370 this.enterHandler(); 371 break; 372 case 'Escape': 373 this.escapeHandler(); 374 break; 375 } 376 } 377 } 378 } 379 } 380 ) 381 } 382} 383 384const suggestControllers = []; 385const initSuggestControllers = () => { 386 suggestControllers.length = 0; 387 const searchForms = document.querySelectorAll('form[data-suggest]') 388 if (searchForms.length) {
389 searchForms.forEach(element => { 390 const controller = new SuggestController(element); 391 controller.init(); 392 suggestControllers.push(controller); 393 }); 394 } 395}; 396 397initSuggestControllers(); 398window.addEventListener('resize', () => { 399 suggestControllers.forEach(controller => controller.setSuggestionBoxWidthAndPosition()); 400}); 401document.body.addEventListener("tx_solr_updated", initSuggestControllers);
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.