1/** 2 * Search functionality for Equine Streaming plugin. 3 */ 4(function($) { 5 'use strict'; 6 7 var searchTimeout = null; 8 var isSearching = false; 9 10 /** 11 * Initialize search functionality. 12 */ 13 function initSearch() { 14 // Open modal when search icon is clicked 15 $(document).on('click', '.equine-search-icon', function(e) { 16 e.preventDefault(); 17 openModal(); 18 }); 19 20 // Close modal when close button is clicked 21 $(document).on('click', '.equine-search-modal-close', function(e) { 22 e.preventDefault(); 23 closeModal(); 24 }); 25 26 // Close modal when overlay is clicked 27 $(document).on('click', '.equine-search-modal-overlay', function(e) { 28 e.preventDefault(); 29 closeModal(); 30 }); 31 32 // Close modal on ESC key 33 $(document).on('keydown', function(e) { 34 if (e.key === 'Escape' || e.keyCode === 27) { 35 if ($('.equine-search-modal').is(':visible')) { 36 closeModal(); 37 } 38 } 39 }); 40 41 // Handle search input 42 $(document).on('input', '.equine-search-input', function() { 43 var query = $(this).val().trim(); 44 45 // Clear previous timeout 46 if (searchTimeout) { 47 clearTimeout(searchTimeout); 48 } 49 50 // If query is less than 3 characters, clear results 51 if (query.length < 3) { 52 clearResults(); 53 return; 54 } 55 56 // Debounce search (wait 300ms after user stops typing) 57 searchTimeout = setTimeout(function() { 58 performSearch(query); 59 }, 300); 60 }); 61 62 // Handle Enter key in search input 63 $(document).on('keydown', '.equine-search-input', function(e) { 64 if (e.key === 'Enter' || e.keyCode === 13) { 65 e.preventDefault(); 66 var query = $(this).val().trim(); 67 if (query.length >= 3) { 68 if (searchTimeout) { 69 clearTimeout(searchTimeout); 70 } 71 performSearch(query); 72 } 73 } 74 }); 75 76 // Handle result click 77 $(document).on('click', '.equine-search-result-item', function(e) { 78 e.preventDefault(); 79 var url = $(this).data('url'); 80 if (url) { 81 window.location.href = url; 82 } 83 }); 84 } 85 86 /** 87 * Open search modal. 88 */ 89 function openModal() { 90 // Hide all elements that might appear above the modal 91 $('body').addClass('equine-search-modal-open'); 92 93 $('.equine-search-modal').css({ 94 'display': 'block', 95 'opacity': '1', 96 'z-index': '99999999' 97 }); 98 $('.equine-search-modal-overlay').css({ 99 'opacity': '1', 100 'background-color': 'rgba(0, 0, 0, 0.75)', 101 'z-index': '99999998' 102 }); 103 $('.equine-search-modal-content').css({ 104 'z-index': '99999999' 105 }); 106 $('.equine-search-input').focus(); 107 $('body').css('overflow', 'hidden'); // Prevent body scroll 108 } 109 110 /** 111 * Close search modal. 112 */ 113 function closeModal() { 114 $('.equine-search-modal').css('display', 'none'); 115 $('.equine-search-input').val(''); 116 clearResults(); 117 $('body').removeClass('equine-search-modal-open'); 118 $('body').css('overflow', ''); // Restore body scroll 119 } 120 121 /** 122 * Perform search via AJAX. 123 * 124 * @param {string} query Search query. 125 */ 126 function performSearch(query) { 127 if (isSearching) { 128 return; // Prevent multiple simultaneous requests 129 } 130 131 if (!query || query.length < 3) { 132 clearResults(); 133 return; 134 } 135 136 isSearching = true; 137 showLoading(); 138 139 $.ajax({ 140 url: equineSearch.ajaxUrl, 141 type: 'POST', 142 data: { 143 action: 'equine_streaming_search', 144 nonce: equineSearch.nonce, 145 query: query 146 }, 147 success: function(response) { 148 isSearching = false; 149 150 if (response.success && response.data && response.data.results) { 151 displayResults(response.data.results); 152 } else { 153 displayNoResults(); 154 } 155 }, 156 error: function() { 157 isSearching = false; 158 displayError(); 159 } 160 }); 161 } 162 163 /** 164 * Display search results. 165 * 166 * @param {Array} results Array of result objects. 167 */ 168 function displayResults(results) { 169 var $resultsContainer = $('.equine-search-results'); 170 $resultsContainer.empty(); 171 172 if (results.length === 0) { 173 displayNoResults(); 174 return; 175 } 176 177 var html = '<div class="equine-search-results-list">'; 178 179 $.each(results, function(index, result) { 180 var thumbnail = result.thumbnail || ''; 181 var duration = result.duration_formatted || ''; 182 var taxonomies = result.taxonomies || ''; 183 184 html += '<div class="equine-search-result-item" data-url="' + escapeHtml(result.url) + '">'; 185 186 html += '<div class="equine-search-result-thumbnail">'; 187 if (thumbnail && thumbnail.trim() !== '') { 188 html += '<img src="' + escapeHtml(thumbnail) + '" alt="' + escapeHtml(result.title) + '" onerror="this.style.display=\'none\'">'; 189 } else {
190 html += '<div class="equine-search-thumbnail-placeholder">ð¹</div>'; 191 } 192 if (duration) { 193 html += '<span class="equine-search-result-duration">' + escapeHtml(duration) + '</span>'; 194 } 195 html += '</div>'; 196 197 html += '<div class="equine-search-result-content">'; 198 html += '<h3 class="equine-search-result-title">' + escapeHtml(result.title) + '</h3>'; 199 if (taxonomies) { 200 html += '<p class="equine-search-result-taxonomies">' + escapeHtml(taxonomies) + '</p>'; 201 } else { 202 html += '<p class="equine-search-result-taxonomies" style="color: #999; font-style: italic;">No categories</p>'; 203 } 204 html += '</div>'; 205 html += '</div>'; 206 }); 207 208 html += '</div>'; 209 $resultsContainer.html(html); 210 } 211 212 /** 213 * Display "no results" message. 214 */ 215 function displayNoResults() { 216 var $resultsContainer = $('.equine-search-results'); 217 $resultsContainer.html( 218 '<div class="equine-search-no-results">' + 219 '<p>' + (typeof equineSearch !== 'undefined' && equineSearch.noResultsText ? equineSearch.noResultsText : 'No results found.') + '</p>' + 220 '</div>' 221 ); 222 } 223 224 /** 225 * Display error message. 226 */ 227 function displayError() { 228 var $resultsContainer = $('.equine-search-results'); 229 $resultsContainer.html( 230 '<div class="equine-search-error">' + 231 '<p>' + (typeof equineSearch !== 'undefined' && equineSearch.errorText ? equineSearch.errorText : 'An error occurred. Please try again.') + '</p>' + 232 '</div>' 233 ); 234 } 235 236 /** 237 * Show loading state. 238 */ 239 function showLoading() { 240 var $resultsContainer = $('.equine-search-results'); 241 $resultsContainer.html( 242 '<div class="equine-search-loading">' + 243 '<div class="equine-search-spinner"></div>' + 244 '<p>Searching...</p>' + 245 '</div>' 246 ); 247 } 248 249 /** 250 * Clear results. 251 */ 252 function clearResults() { 253 var $resultsContainer = $('.equine-search-results'); 254 $resultsContainer.empty(); 255 } 256 257 /** 258 * Escape HTML to prevent XSS. 259 * 260 * @param {string} text Text to escape. 261 * @return {string} Escaped text. 262 */ 263 function escapeHtml(text) { 264 var map = { 265 '&': '&', 266 '<': '<', 267 '>': '>', 268 '"': '"', 269 "'": ''' 270 }; 271 return text.replace(/[&<>"']/g, function(m) { return map[m]; }); 272 } 273 274 // Initialize when DOM is ready 275 $(document).ready(function() { 276 initSearch(); 277 }); 278 279})(jQuery);
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.