1/** 2 * Site Search Functionality 3 * 4 * Handles AJAX search with debouncing and result display. 5 * 6 * @package StudioSwift 7 */ 8 9(function() { 10 'use strict'; 11 12 // Wait for DOM to be ready. 13 document.addEventListener('DOMContentLoaded', function() { 14 initSearch(); 15 }); 16 17 /** 18 * Initialize search functionality 19 */ 20 function initSearch() { 21 const searchModal = document.getElementById('site-search'); 22 const searchInput = document.getElementById('site-search-input'); 23 const searchResults = document.querySelector('.site-search__results ul'); 24 const searchFooter = document.querySelector('.site-search__footer'); 25 const clearButton = document.getElementById('site-search-clear'); 26 27 if (!searchModal || !searchInput || !searchResults) return; 28 29 const resultsContainer = searchResults.closest('.site-search__results'); 30 31 let searchTimeout; 32 let currentRequest; 33 34 // Clear results initially and hide footer 35 clearResults(); 36 if (searchFooter) { 37 searchFooter.style.display = 'none'; 38 } 39 40 // Handle keyup with debouncing 41 searchInput.addEventListener('keyup', function(e) { 42 // Skip if it's just navigation keys 43 if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) { 44 return; 45 } 46 47 const query = this.value.trim(); 48 49 // Clear previous timeout 50 if (searchTimeout) { 51 clearTimeout(searchTimeout); 52 } 53 54 // Cancel previous request 55 if (currentRequest) { 56 currentRequest.abort(); 57 } 58 59 if (query.length === 0) { 60 clearResults(); 61 if (searchFooter) { 62 searchFooter.style.display = 'none'; 63 } 64 return; 65 } 66 67 // Don't show loading immediately, wait for the timeout 68 // Debounce search - wait 1.5 seconds after user stops typing 69 searchTimeout = setTimeout(function() { 70 showLoading(); 71 performSearch(query); 72 }, 700); 73 }); 74 75 // Handle Enter key to perform immediate search 76 searchInput.addEventListener('keydown', function(e) { 77 if (e.key === 'Enter') { 78 e.preventDefault(); 79 const query = this.value.trim(); 80 81 if (query.length > 0) { 82 // Clear timeout and perform immediate search 83 if (searchTimeout) { 84 clearTimeout(searchTimeout); 85 } 86 87 // Cancel previous request 88 if (currentRequest) { 89 currentRequest.abort(); 90 } 91 92 showLoading(); 93 performSearch(query); 94 } 95 } 96 }); 97 98 // Clear button functionality 99 if (clearButton) { 100 clearButton.addEventListener('click', function() { 101 searchInput.value = ''; 102 clearResults(); 103 if (searchFooter) { 104 searchFooter.style.display = 'none'; 105 } 106 searchInput.focus(); 107 }); 108 } 109 110 /** 111 * Perform AJAX search 112 */ 113 function performSearch(query) { 114 if (!window.studioswiftData) { 115 console.error('StudioSwift: AJAX data not available'); 116 return; 117 } 118 119 const formData = new FormData(); 120 formData.append('action', 'studioswift_search'); 121 formData.append('query', query); 122 formData.append('nonce', window.studioswiftData.nonce); 123 124 currentRequest = new XMLHttpRequest(); 125 currentRequest.open('POST', window.studioswiftData.ajaxUrl); 126 127 currentRequest.onload = function() { 128 if (this.status === 200) { 129 try { 130 const response = JSON.parse(this.responseText); 131 if (response.success) { 132 displayResults(response.data, query); 133 } else { 134 showError('Search failed. Please try again.'); 135 } 136 } catch (e) { 137 showError('Search failed. Please try again.'); 138 } 139 } else { 140 showError('Search failed. Please try again.'); 141 } 142 currentRequest = null; 143 }; 144 145 currentRequest.onerror = function() { 146 showError('Search failed. Please try again.'); 147 currentRequest = null; 148 }; 149 150 currentRequest.send(formData); 151 } 152 153 /** 154 * Display search results 155 */ 156 function displayResults(data, query) { 157 if (data.results && data.results.length > 0) { 158 // Show results 159 let html = ''; 160 data.results.forEach(function(result) { 161 html += ` 162 <li> 163 <div class="ss search-result"> 164 <a class="l-link" href="${result.permalink}" role="menuitem">Read more</a> 165 <figure> 166 <img src="${result.thumbnail}" 167 width="135" height="135" alt="${escapeHtml(result.title)}"> 168 </figure> 169 <article> 170 <h6>${escapeHtml(result.title)}</h6> 171 <p class="search-result__desc">${escapeHtml(result.post_type)}</p> 172 </article> 173 </div> 174 </li> 175 `; 176 }); 177 178 searchResults.innerHTML = html; 179 180 // Announce results to screen readers 181 const resultsContainer = searchResults.closest('.site-search__results'); 182 if (resultsContainer) { 183 resultsContainer.setAttribute('aria-label', `${data.results.length} search results found for ${query}`); 184 } 185
186 // Show footer with "View all results" link 187 if (searchFooter) { 188 const footerLink = searchFooter.querySelector('a'); 189 const footerText = searchFooter.querySelector('.js-search-footer-text'); 190 191 if (footerLink && footerText) { 192 // Update only the text span, leaving the icon intact 193 footerText.textContent = `View all results for "${query}"`; 194 footerLink.href = `${window.studioswiftData.siteUrl}?s=${encodeURIComponent(query)}`; 195 footerLink.setAttribute('title', `View all results for ${escapeHtml(query)}`); 196 } 197 searchFooter.style.display = 'block'; 198 } 199 } else { 200 // Show no results message 201 const message = data.message || `No results found for "${query}".`; 202 searchResults.innerHTML = ` 203 <li> 204 <div class="ss search-result search-result--no-results"> 205 <h4>${escapeHtml(message)}</h4> 206 <p>Check the spelling or use a different word or phrase.</p> 207 </div> 208 </li> 209 `; 210 211 // Announce no results to screen readers 212 const resultsContainer = searchResults.closest('.site-search__results'); 213 if (resultsContainer) { 214 resultsContainer.setAttribute('aria-label', `No search results found for ${query}.Check the spelling or use a different word or phrase.`); 215 } 216 217 if (searchFooter) { 218 searchFooter.style.display = 'none'; 219 } 220 } 221 } 222 223 /** 224 * Show loading state 225 */ 226 function showLoading() { 227 if (resultsContainer) resultsContainer.style.display = ''; 228 searchResults.innerHTML = ` 229 <li> 230 <div class="ss search-result search-result--loading"> 231 <div class="search-loader"> 232 <div class="search-loader__spinner"></div> 233 <p>Searching...</p> 234 </div> 235 </div> 236 </li> 237 `; 238 239 if (searchFooter) { 240 searchFooter.style.display = 'none'; 241 } 242 } 243 244 /** 245 * Show error message 246 */ 247 function showError(message) { 248 searchResults.innerHTML = ` 249 <li> 250 <div class="ss search-result search-result--error"> 251 <p>${escapeHtml(message)}</p> 252 </div> 253 </li> 254 `; 255 256 if (searchFooter) { 257 searchFooter.style.display = 'none'; 258 } 259 } 260 261 /** 262 * Clear search results 263 */ 264 function clearResults() { 265 searchResults.innerHTML = ''; 266 if (resultsContainer) resultsContainer.style.display = 'none'; 267 } 268 269 /** 270 * Escape HTML to prevent XSS 271 */ 272 function escapeHtml(text) { 273 const div = document.createElement('div'); 274 div.textContent = text; 275 return div.innerHTML; 276 } 277 } 278 279})();
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.