1/** 2 * ============================================================ 3 * LIVE SEARCH v2.0 â Frontend JavaScript (FIXED) 4 * ============================================================ 5 * 6 * Fixed issues: 7 * - Works with both #ls-input and .ls-search-input selectors 8 * - Works with #ls-dropdown for both popup and search page 9 * - Proper form handling with id="ls-form" 10 * - Autocomplete dropdown suggestions 11 * - Keyboard navigation (arrow keys, enter, escape) 12 * - Click outside to close 13 * ============================================================ 14 */ 15 16(function() { 17 'use strict'; 18 19 const liveSearch = { 20 // DOM elements 21 input: null, 22 dropdown: null, 23 form: null, 24 25 // State 26 isOpen: false, 27 debounceTimer: null, 28 selectedIndex: -1, 29 currentKeyword: '', 30 31 /** 32 * Initialize live search 33 */ 34 init: function() { 35 // Support both #ls-input (popup/search page) and .ls-search-input (custom class) 36 this.input = document.querySelector('#ls-input') || document.querySelector('.ls-search-input'); 37 this.dropdown = document.querySelector('#ls-dropdown') || document.querySelector('.ls-dropdown'); 38 this.form = document.querySelector('#ls-form') || document.querySelector('.ls-search-form'); 39 40 if ( ! this.input || ! this.dropdown ) { 41 console.warn('Live Search: Missing required elements (input or dropdown)'); 42 return; 43 } 44 45 // Bind events 46 this.input.addEventListener('input', (e) => this.onInput(e)); 47 this.input.addEventListener('keydown', (e) => this.onKeyDown(e)); 48 this.input.addEventListener('focus', (e) => this.onFocus(e)); 49 document.addEventListener('click', (e) => this.onDocumentClick(e)); 50 51 // Submit form with Enter key 52 if ( this.form ) { 53 this.form.addEventListener('submit', (e) => this.onFormSubmit(e)); 54 } 55 56 console.log('Live Search: Initialized successfully'); 57 }, 58 59 /** 60 * Handle input event 61 */ 62 onInput: function(e) { 63 const keyword = e.target.value.trim(); 64 65 // Clear previous debounce 66 if ( this.debounceTimer ) { 67 clearTimeout(this.debounceTimer); 68 } 69 70 // If keyword is too short, close dropdown 71 if ( keyword.length < liveSearchData.minChars ) { 72 this.close(); 73 return; 74 } 75 76 // Debounce the AJAX call 77 this.debounceTimer = setTimeout(() => { 78 this.fetchSuggestions(keyword); 79 }, 300); 80 }, 81 82 /** 83 * Handle keyboard navigation 84 */ 85 onKeyDown: function(e) { 86 const items = this.dropdown.querySelectorAll('.ls-suggestion-item'); 87 88 switch(e.key) { 89 case 'ArrowDown': 90 e.preventDefault(); 91 if ( ! this.isOpen ) return; 92 93 if ( this.selectedIndex < items.length - 1 ) { 94 this.selectedIndex++; 95 this.updateSelected(items); 96 } 97 break; 98 99 case 'ArrowUp': 100 e.preventDefault(); 101 if ( ! this.isOpen ) return; 102 103 if ( this.selectedIndex > 0 ) { 104 this.selectedIndex--; 105 this.updateSelected(items); 106 } 107 break; 108 109 case 'Enter': 110 e.preventDefault(); 111 // Always navigate to full results page 112 this.submitSearch(); 113 break; 114 115 case 'Escape': 116 e.preventDefault(); 117 this.close(); 118 break; 119 } 120 }, 121 122 /** 123 * Handle focus event 124 */ 125 onFocus: function(e) {
126 const keyword = e.target.value.trim(); 127 if ( keyword.length >= liveSearchData.minChars && this.dropdown.innerHTML.trim() ) { 128 this.open(); 129 } 130 }, 131 132 /** 133 * Handle document click (close dropdown if clicking outside) 134 */ 135 onDocumentClick: function(e) { 136 // Close if clicking outside input and dropdown 137 const isClickInside = this.input.contains(e.target) || this.dropdown.contains(e.target); 138 if ( ! isClickInside && this.isOpen ) { 139 this.close(); 140 } 141 }, 142 143 /** 144 * Handle form submission 145 */ 146 onFormSubmit: function(e) { 147 e.preventDefault(); 148 this.submitSearch(); 149 }, 150 151 /** 152 * Submit search and navigate to results page 153 */ 154 submitSearch: function() { 155 const keyword = this.input.value.trim(); 156 157 if ( keyword.length < liveSearchData.minChars ) { 158 this.input.focus(); 159 return; 160 } 161 162 // Navigate to search results page with keyword 163 const url = liveSearchData.resultsPageUrl + encodeURIComponent(keyword); 164 window.location.href = url; 165 }, 166 167 /** 168 * Fetch autocomplete suggestions via AJAX 169 */ 170 fetchSuggestions: function(keyword) { 171 this.currentKeyword = keyword; 172 173 const formData = new FormData(); 174 formData.append('action', liveSearchData.action); 175 formData.append('nonce', liveSearchData.nonce); 176 formData.append('keyword', keyword); 177 178 fetch(liveSearchData.ajaxUrl, { 179 method: 'POST', 180 body: formData 181 }) 182 .then(response => { 183 if ( ! response.ok ) { 184 throw new Error('Network response was not ok'); 185 } 186 return response.text(); 187 }) 188 .then(html => { 189 // Update dropdown with suggestions 190 if ( html.trim() ) { 191 this.dropdown.innerHTML = html; 192 this.selectedIndex = -1; 193 this.open(); 194 195 // Bind click handlers to suggestion items 196 this.bindSuggestionClicks(); 197 } else { 198 this.close(); 199 } 200 }) 201 .catch(error => { 202 console.error('Live search error:', error); 203 this.close(); 204 }); 205 }, 206 207 /** 208 * Bind click handlers to suggestion items 209 */ 210 bindSuggestionClicks: function() { 211 const items = this.dropdown.querySelectorAll('.ls-suggestion-item'); 212 213 items.forEach((item, index) => { 214 item.addEventListener('click', (e) => { 215 e.preventDefault(); 216 e.stopPropagation(); 217 218 // Set the input to the suggestion text 219 const textSpan = item.querySelector('.ls-suggestion-text'); 220 if ( textSpan ) { 221 this.input.value = textSpan.textContent.trim(); 222 } 223 this.close(); 224 // Navigate to full results page 225 this.submitSearch(); 226 }); 227 228 item.addEventListener('mouseenter', () => { 229 this.selectedIndex = index; 230 this.updateSelected(items); 231 }); 232 }); 233 234 // Also bind the "View all results" footer link 235 const footerLink = this.dropdown.querySelector('.ls-see-all-link'); 236 if ( footerLink ) { 237 footerLink.addEventListener('click', (e) => { 238 e.preventDefault(); 239 const url = footerLink.getAttribute('href'); 240 window.location.href = url; 241 }); 242 } 243 }, 244 245 /** 246 * Update visual selection 247 */ 248 updateSelected: function(items) {
249 items.forEach((item, index) => { 250 if ( index === this.selectedIndex ) { 251 item.classList.add('selected'); 252 item.scrollIntoView({ block: 'nearest' }); 253 } else { 254 item.classList.remove('selected'); 255 } 256 }); 257 }, 258 259 /** 260 * Open dropdown 261 */ 262 open: function() { 263 if ( ! this.dropdown.innerHTML.trim() ) { 264 return; 265 } 266 267 this.isOpen = true; 268 this.dropdown.style.display = 'block'; 269 }, 270 271 /** 272 * Close dropdown 273 */ 274 close: function() { 275 this.isOpen = false; 276 this.dropdown.style.display = 'none'; 277 this.selectedIndex = -1; 278 } 279 }; 280 281 // Initialize when DOM is ready 282 if ( document.readyState === 'loading' ) { 283 document.addEventListener('DOMContentLoaded', () => liveSearch.init()); 284 } else { 285 liveSearch.init(); 286 } 287 288 // Expose to window for debugging 289 window.liveSearch = liveSearch; 290})();
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.