1/** 2 * @package OUI Search Module 3 * @subpackage mod_oui_search 4 * 5 * @copyright (C) 2025 Pros.bzh. All rights reserved. 6 * @license GNU General Public License version 2 or later; see LICENSE.txt 7 */ 8 9(function() { 10 'use strict'; 11 12 // Wait for DOM to be ready 13 document.addEventListener('DOMContentLoaded', function() { 14 initializeOuiSearch(); 15 }); 16 17 function initializeOuiSearch() { 18 // Find all OUI Search modules on the page 19 const searchModules = document.querySelectorAll('.mod-oui-search'); 20 21 searchModules.forEach(function(module) { 22 const moduleId = module.id; 23 const paramsData = module.getAttribute('data-params'); 24 25 if (!paramsData) { 26 console.warn('OUI Search: No parameters found for module', moduleId); 27 return; 28 } 29 30 try { 31 const params = JSON.parse(paramsData); 32 initializeModule(module, params); 33 } catch (error) { 34 console.error('OUI Search: Error parsing parameters for module', moduleId, error); 35 } 36 }); 37 } 38 39 function initializeModule(module, params) { 40 const input = module.querySelector('.oui-search-input'); 41 const resultsContainer = module.querySelector('.oui-search-results'); 42 const resultsList = module.querySelector('.oui-search-results-list'); 43 const loadingIndicator = module.querySelector('.oui-search-loading'); 44 45 if (!input || !resultsContainer || !resultsList || !loadingIndicator) { 46 console.error('OUI Search: Required elements not found in module', module.id); 47 return; 48 } 49 50 let searchTimeout; 51 let currentRequest; 52 53 // Input event handler 54 input.addEventListener('input', function(e) { 55 const query = e.target.value.trim(); 56 57 // Console log every keystroke 58 console.log('User typed:', e.target.value, 'Query after trim:', query); 59 60 // Clear previous timeout 61 if (searchTimeout) { 62 clearTimeout(searchTimeout); 63 } 64 65 // Show looking message immediately when user starts typing 66 if (e.target.value.length > 0) { 67 const lookingElement = document.getElementById('oui-search-looking-' + module.id); 68 if (lookingElement) { 69 lookingElement.style.display = 'block'; 70 } 71 } 72 73 // Cancel previous request 74 if (currentRequest) { 75 currentRequest.abort(); 76 currentRequest = null; 77 } 78 79 // Hide results if query is too short 80 if (query.length < params.minChars) { 81 console.log('Query too short, hiding results'); 82 hideResults(); 83 // Hide looking message when query is too short 84 const lookingElement = document.getElementById('oui-search-looking-' + module.id.replace('mod-oui-search-', '')); 85 if (lookingElement) { 86 lookingElement.style.display = 'none'; 87 } 88 return; 89 } 90 91 // Show looking message immediately when typing starts 92 showLoading(); 93 94 // Show loading and delay search 95 console.log('Query meets minimum characters, scheduling search...'); 96 97 searchTimeout = setTimeout(function() { 98 console.log('Executing search for query:', query); 99 performSearch(query); 100 }, 300); // 300ms delay 101 }); 102 103 // Hide results when clicking outside 104 document.addEventListener('click', function(e) { 105 if (!module.contains(e.target)) { 106 hideResults(); 107 } 108 }); 109 110 // Show results when focusing on input (if there are results) 111 input.addEventListener('focus', function() { 112 if (resultsList.children.length > 0) { 113 showResults(); 114 } 115 }); 116 117 // Keyboard navigation 118 input.addEventListener('keydown', function(e) { 119 const items = resultsList.querySelectorAll('.oui-search-result-item'); 120 const activeItem = resultsList.querySelector('.oui-search-result-item.active'); 121 122 if (e.key === 'ArrowDown') { 123 e.preventDefault(); 124 navigateResults(items, activeItem, 'down'); 125 } else if (e.key === 'ArrowUp') { 126 e.preventDefault(); 127 navigateResults(items, activeItem, 'up'); 128 } else if (e.key === 'Enter') { 129 e.preventDefault(); 130 const activeLink = resultsList.querySelector('.oui-search-result-item.active'); 131 if (activeLink) { 132 window.location.href = activeLink.href; 133 } 134 } else if (e.key === 'Escape') { 135 hideResults(); 136 input.blur(); 137 } 138 }); 139 140 function performSearch(query) { 141 // Create FormData for the request 142 const formData = new FormData(); 143 formData.append('option', 'com_ajax'); 144 formData.append('module', 'oui_search'); 145 formData.append('method', 'getAjaxSearch'); 146 formData.append('format', 'json'); 147 formData.append('query', query); 148 formData.append('max_results', params.maxResults); 149 formData.append('show_featured_first', params.showFeaturedFirst ? '1' : '0'); 150 formData.append('show_introtext', params.showIntrotext ? '1' : '0'); 151 formData.append('show_images', params.showImages ? '1' : '0'); 152 formData.append('introtext_limit', params.introtextLimit); 153 154 // Add category filter parameters 155 if (params.categoryFilterType) { 156 formData.append('category_filter_type', params.categoryFilterType); 157 } 158 if (params.categories) { 159 formData.append('categories', params.categories); 160 } 161 162 // Create and send request 163 console.log('=== AJAX CALL INITIATED ==='); 164 console.log('Sending AJAX request to:', params.ajaxUrl || 'index.php'); 165 console.log('Query being searched:', query); 166 console.log('Module ID:', module.id); 167 currentRequest = new XMLHttpRequest(); 168 currentRequest.open('POST', params.ajaxUrl || 'index.php', true); 169 170 currentRequest.onreadystatechange = function() { 171 if (currentRequest.readyState === 4) { 172 hideLoading(); 173 174 if (currentRequest.status === 200) { 175 try { 176 console.log('Raw response:', currentRequest.responseText); 177
178 // Extract JSON from response (handle PHP warnings/notices) 179 let jsonText = currentRequest.responseText; 180 const jsonStart = jsonText.indexOf('{'); 181 const jsonEnd = jsonText.lastIndexOf('}') + 1; 182 183 if (jsonStart !== -1 && jsonEnd > jsonStart) { 184 jsonText = jsonText.substring(jsonStart, jsonEnd); 185 console.log('Extracted JSON:', jsonText); 186 } 187 188 const response = JSON.parse(jsonText); 189 console.log('Parsed response:', response); 190 191 if (response.success && response.data) { 192 console.log('Displaying results:', response.data); 193 displayResults(response.data, query); 194 } else { 195 console.log('No results found'); 196 displayNoResults(); 197 } 198 } catch (error) { 199 console.error('JSON parsing error:', error); 200 console.error('Response text:', currentRequest.responseText); 201 displayError(); 202 } 203 } else if (currentRequest.status !== 0) { // 0 means aborted 204 console.error('HTTP error:', currentRequest.status); 205 displayError(); 206 } 207 208 currentRequest = null; 209 } 210 }; 211 212 currentRequest.send(formData); 213 } 214 215 function displayResults(results, query) { 216 resultsList.innerHTML = ''; 217 218 if (results.length === 0) { 219 displayNoResults(); 220 return; 221 } 222 223 results.forEach(function(result) { 224 const item = createResultItem(result, query); 225 resultsList.appendChild(item); 226 }); 227 228 showResults(); 229 } 230 231 function createResultItem(result, query) { 232 console.log('Creating result item:', result); 233 234 const item = document.createElement('a'); 235 item.className = 'oui-search-result-item'; 236 item.href = result.link; 237 238 if (result.featured) { 239 item.classList.add('featured'); 240 } 241 242 const content = document.createElement('div'); 243 content.className = 'oui-search-result-content'; 244 245 // Add intro image if available 246 if (result.image) { 247 console.log('Adding image:', result.image); 248 const imageDiv = document.createElement('div'); 249 imageDiv.className = 'oui-search-result-image'; 250 251 const img = document.createElement('img'); 252 img.src = result.image; 253 img.alt = result.title || 'Article image'; 254 img.loading = 'lazy'; 255 img.onerror = function() { 256 console.log('Image failed to load:', result.image); 257 imageDiv.style.display = 'none'; 258 }; 259 260 imageDiv.appendChild(img); 261 content.appendChild(imageDiv); 262 } 263 264 // Text content 265 const textDiv = document.createElement('div'); 266 textDiv.className = 'oui-search-result-text'; 267 268 // Title (always show) 269 const title = document.createElement('h4'); 270 title.className = 'oui-search-result-title'; 271 title.innerHTML = highlightSearchTerms(result.title || 'Untitled', query); 272 textDiv.appendChild(title); 273 console.log('Added title:', result.title); 274 275 // Category 276 if (result.category) { 277 const category = document.createElement('div'); 278 category.className = 'oui-search-result-category'; 279 category.textContent = result.category; 280 textDiv.appendChild(category); 281 } 282 283 // Introtext (always show if available) 284 if (result.introtext) { 285 console.log('Adding introtext:', result.introtext); 286 const introtext = document.createElement('p'); 287 introtext.className = 'oui-search-result-introtext'; 288 introtext.innerHTML = highlightSearchTerms(result.introtext, query); 289 textDiv.appendChild(introtext); 290 } 291 292 content.appendChild(textDiv); 293 item.appendChild(content); 294 295 // Add click handler 296 item.addEventListener('mouseenter', function() { 297 clearActiveItems(); 298 item.classList.add('active'); 299 }); 300 301 return item; 302 } 303 304 function highlightSearchTerms(text, query) { 305 if (!query || query.length < 2) { 306 return text; 307 } 308 309 const regex = new RegExp('(' + escapeRegExp(query) + ')', 'gi'); 310 return text.replace(regex, '<span class="oui-search-highlight">$1</span>'); 311 } 312 313 function escapeRegExp(string) { 314 return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 315 } 316 317 function displayNoResults() { 318 resultsList.innerHTML = '<div class="oui-search-no-results">' + params.textNoResults + '</div>'; 319 showResults(); 320 } 321 322 function displayError() { 323 resultsList.innerHTML = '<div class="oui-search-no-results">Search error occurred. Please try again.</div>'; 324 showResults(); 325 } 326 327 function showLoading() { 328 loadingIndicator.style.display = 'block'; 329 } 330 331 function hideLoading() { 332 loadingIndicator.style.display = 'none'; 333 334 // Also hide the looking message 335 const lookingElement = document.getElementById('oui-search-looking-' + module.id); 336 if (lookingElement) { 337 lookingElement.style.display = 'none'; 338 } 339 } 340 341 function showResults() { 342 resultsContainer.style.display = 'block'; 343 } 344 345 function hideResults() { 346 resultsContainer.style.display = 'none'; 347 clearActiveItems(); 348 } 349 350 function navigateResults(items, activeItem, direction) { 351 if (items.length === 0) return; 352 353 let newIndex = -1; 354 355 if (activeItem) { 356 const currentIndex = Array.from(items).indexOf(activeItem); 357 newIndex = direction === 'down' ? currentIndex + 1 : currentIndex - 1; 358 } else { 359 newIndex = direction === 'down' ? 0 : items.length - 1; 360 } 361 362 // Wrap around 363 if (newIndex >= items.length) newIndex = 0; 364 if (newIndex < 0) newIndex = items.length - 1; 365 366 clearActiveItems(); 367 items[newIndex].classList.add('active'); 368 369 // Scroll into view if needed 370 items[newIndex].scrollIntoView({ 371 block: 'nearest', 372 behavior: 'smooth' 373 }); 374 } 375 376 function clearActiveItems() { 377 const activeItems = resultsList.querySelectorAll('.oui-search-result-item.active');
378 activeItems.forEach(function(item) { 379 item.classList.remove('active'); 380 }); 381 } 382 } 383})();
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.