1jQuery(function ($) { 2 const searchInput = document.querySelector('.meili-autocomplete-input'); 3 if (!searchInput) { 4 return; 5 } 6 7 let container = searchInput.closest('.autocomplete-container'); 8 if (!container) { 9 container = document.createElement('div'); 10 container.className = 'autocomplete-container'; 11 searchInput.parentNode.insertBefore(container, searchInput); 12 container.appendChild(searchInput); 13 } 14 15 searchInput.style.display = 'none'; 16 17 const autocompleteLib = window['@algolia/autocomplete-js']?.autocomplete; 18 if (!autocompleteLib) { 19 //console.error('Autocomplete: window["@algolia/autocomplete-js"].autocomplete not available'); 20 return; 21 } 22 23 function triggerSearchResults(query) { 24 if (!query || !query.trim()) return; 25 //console.log('Triggering page results for:', query); 26 27 if (typeof loadMeiliResults === 'function') { 28 loadMeiliResults(query, 1); 29 } else { 30 //console.warn('loadMeiliResults() not found � falling back to basic result render.'); 31 } 32 } 33 34 // Helper function to decode HTML entities 35 function decodeHtmlEntities(str) { 36 const textarea = document.createElement('textarea'); 37 textarea.innerHTML = str; 38 return textarea.value; 39 } 40 function toSlug(str) { 41 return str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, ''); 42 } 43 // Helper function to get the icon based on post type and media_types 44 function getPostTypeIcon(postType, mediaTypes, html) { 45 const iconMap = { 46 visual_whoi_item: { 47 image: html`<i class="fas fa-image"></i>`, 48 video: html`<i class="fas fa-play-circle"></i>`, 49 illustration: html`<i class="fas fa-palette"></i>`, 50 infographic: html`<i class="fas fa-chart-pie"></i>`, 51 'interactive-science': html`<i class="fas fa-bullseye-pointer"></i>`, 52 map: html`<i class="fas fa-map"></i>`, 53 'virtual-series': html`<i class="fas fa-play-circle"></i>`, 54 slideshow: html`<i class="fas fa-images"></i>`, 55 }, 56 page: html`<i class="fas fa-list"></i>`, 57 ocean_fact: html`<i class="fas fa-check"></i>` 58 }; 59 60 61 62 if (postType === 'visual_whoi_item' && mediaTypes && mediaTypes.length > 0) { 63 const mediaType = toSlug(mediaTypes[0]); 64 return iconMap.visual_whoi_item[mediaType] || ''; 65 } 66 return iconMap[postType] || ''; 67 } 68 69 70 const { autocomplete, html } = window['@algolia/autocomplete-js']; 71 const autocompleteInstance = autocomplete({ 72 container: container, 73 detachedMediaQuery: 'none', 74 placeholder: 'Search...', 75 debug: true, // Enable debug mode for better logging 76 getSources({ query, setQuery }) { 77 //console.log('Autocomplete: getSources called with query:', query); 78 79 const aaInput = document.querySelector('.aa-Input'); 80 if (aaInput?.getAttribute('data-autocomplete-locked') === 'true') { 81 //console.log('Autocomplete: temporarily locked, skipping dropdown sources'); 82 return []; 83 } 84 85 setQuery(query || ''); 86 return [ 87 { 88 sourceId: 'meilisearch', 89 getItems() { 90 //console.log('Autocomplete: Sending AJAX request for query:', query); 91 if (!window.meiliSearch?.ajaxUrl) { 92 //console.error('Autocomplete: meiliSearch.ajaxUrl not defined'); 93 return []; 94 } 95 return $.ajax({ 96 url: window.meiliSearch.ajaxUrl, 97 method: 'POST', 98 data: { 99 action: 'meili_search_ajax', 100 query: query || '', 101 index: 'learning_content', 102 page: 1, 103 filter: '', 104 limit: 10, 105 attributesToRetrieve: JSON.stringify(['id', 'title', 'permalink', 'post_type', 'topics_names', 'media_types_names']) 106 }, 107 dataType: 'json', 108 error: (xhr, status, error) => { 109 //console.error('Autocomplete: AJAX request failed:', { status, error, xhr });
110 } 111 }).then(response => { 112 //console.log('Autocomplete: AJAX response:', response); 113 if (response && response.success && response.data && response.data.hits) { 114 let topicSet = new Set(); 115 let mediatypesSet = new Set(); 116 let hasInsertedHeader = false; 117 118 const hits = response.data.hits.slice(0, 6); 119 120 const items = hits.map((hit, index) => { 121 // Collect topics from topics_names (with fallback) 122 const topics = Array.isArray(hit.topics_names) ? hit.topics_names : []; 123 topics.forEach(name => topicSet.add(name)); 124 125 const mediatypes = Array.isArray(hit.media_types_names) ? hit.media_types_names : []; 126 mediatypes.forEach(name => mediatypesSet.add(name)); 127 128 const item = { 129 id: hit.id, 130 title: hit.title || '(Untitled)', 131 permalink: hit.permalink || 'https://hub.whoi.edu', 132 post_type: hit.post_type || 'unknown', 133 topics_names: topics, 134 media_types: mediatypes, 135 }; 136 137 if (!hasInsertedHeader) { 138 item._showHeader = true; 139 hasInsertedHeader = true; 140 } 141 142 return item; 143 }); 144 145 window.meiliSearchRecommendedTopics = Array.from(topicSet); 146 //console.log('Autocomplete: Collected topics:', window.meiliSearchRecommendedTopics); 147 return items; 148 } 149 150 //console.warn('Autocomplete: No valid hits in response', response); 151 return []; 152 }).catch(error => { 153 //console.error('Autocomplete: AJAX promise error:', error); 154 return []; 155 }); 156 }, 157 templates: { 158 item({ item, html }) { 159 const headerHtml = item._showHeader 160 ? html`<div class="autocomplete-header">Recommended Results</div>` 161 : ''; 162 163 const decodedTitle = decodeHtmlEntities(item.title); 164 const icon = getPostTypeIcon(item.post_type, item.media_types, html); 165 // Log media_types to debug the data structure 166 //console.log('Item media_types for', item.title, ':', item.media_types); 167 168 // Normalize media_types to ensure it's an array and join with commas 169 let detailText; 170 if (item.media_types && item.media_types.length > 0) { 171 detailText = item.media_types.join(', '); 172 } else if (item.article_type) { 173 detailText = item.article_type; 174 } else { 175 detailText = item.post_type; 176 } 177 178 179 return html`${headerHtml} 180 <div class="aa-ItemWrapper"> 181 <div class="aa-ItemContent" style="display: flex; align-items: center;"> 182 <div> 183 <div class="aa-ItemDetails">
184 <span class="aa-ItemPostType">${detailText}</span> 185 </div> 186 <div class="aa-ItemTitle">${decodedTitle}</div> 187 </div> 188 </div> 189 </div>`; 190 }, 191 noResults({ html }) { 192 //console.log('Autocomplete: No results'); 193 return html`<div class="aa-NoResults">No results found.</div>`; 194 }, 195 //footer({ html }) { 196 // const topics = window.meiliSearchRecommendedTopics || []; 197 //console.log('Autocomplete: Rendering footer with topics:', topics); 198 // if (!topics.length) { 199 // //console.log('Autocomplete: No topics to display in footer'); 200 // return null; 201 // } 202 203 // const toSlug = str => str.toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, ''); 204 205 // return html` 206 // <div class="autocomplete-footer"> 207 // <div class="autocomplete-header">Recommended Topics</div> 208 // ${topics.map(topic => { 209 // const slug = toSlug(topic); 210 // const decoded = decodeHtmlEntities(topic); 211 // return html` 212 // <div class="autocomplete-topic" data-topic-slug="${slug}" data-topic-name="${decoded}"> 213 // ${decoded} 214 // </div> 215 // `; 216 // })} 217 // </div> 218 // `; 219 220 //} 221 }, 222 onSelect({ item }) { 223 //console.log('Autocomplete: Selected item:', item); 224 225 const aaInput = document.querySelector('.aa-Input'); 226 if (aaInput) { 227 aaInput.value = item.title; 228 aaInput.setAttribute('data-autocomplete-locked', 'true'); 229 aaInput.dispatchEvent(new Event('input', { bubbles: true })); 230 aaInput.dispatchEvent(new Event('change', { bubbles: true })); 231 232 triggerSearchResults(item.title); 233 } 234 235 document.querySelector('.aa-Panel')?.classList.add('hidden'); 236 } 237 } 238 ]; 239 } 240 }); 241 242 document.addEventListener('input', function (e) { 243 if (e.target.classList.contains('aa-Input') && e.target.getAttribute('data-autocomplete-locked') === 'true') { 244 e.target.removeAttribute('data-autocomplete-locked'); 245 //console.log('Autocomplete re-enabled due to user typing'); 246 } 247 }); 248 249 document.addEventListener('focusin', function (e) { 250 const aaInput = e.target.closest('.aa-Input'); 251 if (aaInput && aaInput.getAttribute('data-autocomplete-locked') === 'true') { 252 aaInput.removeAttribute('data-autocomplete-locked'); 253 aaInput.dispatchEvent(new Event('input', { bubbles: true })); 254 //console.log('Autocomplete unlocked on input focus'); 255 } 256 }); 257 258 document.addEventListener('submit', function (e) { 259 const aaForm = e.target.closest('.aa-Form'); 260 const meiliForm = e.target.closest('.meili-text-search-form'); 261 if (!aaForm && !meiliForm) return; 262 263 //console.log('[DEBUG] Submit event triggered. Form:', aaForm ? '.aa-Form' : '.meili-text-search-form', 'Target:', e.target.tagName, 'Default prevented:', e.defaultPrevented); 264 265 e.preventDefault(); 266 267 const aaInput = document.querySelector('.aa-Input'); 268 const query = aaInput?.value.trim(); 269 270 //console.log('[DEBUG] Submit query:', query); 271 272 if (query && typeof loadMeiliResults === 'function') { 273 const resultsBox = document.querySelector('#meili-results'); 274 //console.log('[DEBUG] Results box exists:', !!resultsBox); 275 // No replaceState here: loadMeiliResults() rewrites the URL itself 276 // once results arrive, preserving params this plugin doesn't own. 277 // The old '?meili_query=' write clobbered the whole query string and 278 // set a key nothing reads (the results page reads 'q'). 279 loadMeiliResults(query, 1); 280 if (autocompleteInstance) { 281 autocompleteInstance.setIsOpen(false); 282 //console.log('Autocomplete: Panel closed via form submit'); 283 } 284 } else { 285 //console.warn('[DEBUG] Query empty or loadMeiliResults unavailable.'); 286 } 287 });
288 289 document.addEventListener('click', function (e) { 290 const topicEl = e.target.closest('.autocomplete-topic'); 291 const aaInput = document.querySelector('.aa-Input'); 292 const aaPanel = document.querySelector('.aa-Panel'); 293 const aaSubmit = document.querySelector('.aa-SubmitButton, .meili-search-submit, button[type="submit"]'); 294 295 //console.log('[DEBUG] Click event. Submit button exists:', !!aaSubmit, 'Classes:', aaSubmit?.className, 'Target:', e.target.tagName); 296 297 if (!aaInput || !aaPanel) return; 298 299 // Handle topic clicks 300 if (topicEl) { 301 const slug = topicEl.dataset.topicSlug; 302 const name = topicEl.dataset.topicName; 303 304 const checkboxes = document.querySelectorAll(`input[type="checkbox"][name="topics[]"]`); 305 checkboxes.forEach(cb => { 306 if (cb.value !== slug && cb.checked) { 307 cb.checked = false; 308 } 309 }); 310 311 const targetCheckbox = Array.from(checkboxes).find(cb => cb.value === slug); 312 if (targetCheckbox && !targetCheckbox.checked) { 313 targetCheckbox.checked = true; 314 const currentLi = document.querySelector(`li[data-topic-slug="${slug}"]`); 315 const parentUl = currentLi?.closest('.meili-subterms'); 316 const parentLi = parentUl?.closest('li[data-topic-slug]'); 317 if (parentLi && !parentLi.classList.contains('expanded')) { 318 parentLi.classList.add('expanded'); 319 } 320 } 321 322 const filtersForm = document.getElementById('meili-filters'); 323 if (filtersForm) { 324 filtersForm.dispatchEvent(new Event('change', { bubbles: true })); 325 } 326 327 if (aaInput) { 328 aaInput.value = name; 329 aaInput.setAttribute('data-autocomplete-locked', 'true'); 330 aaInput.dispatchEvent(new Event('input', { bubbles: true })); 331 aaInput.dispatchEvent(new Event('change', { bubbles: true })); 332 } 333 334 if (autocompleteInstance) { 335 autocompleteInstance.setIsOpen(false); 336 //console.log('Autocomplete: Panel closed via topic click'); 337 } 338 return; 339 } 340 341 // Handle submit button clicks 342 if (aaSubmit && (aaSubmit.contains(e.target) || e.target.closest('.aa-SubmitButton, .meili-search-submit, button[type="submit"]'))) { 343 //console.log('[DEBUG] Submit button clicked. Target:', e.target.tagName); 344 const form = aaSubmit.closest('.aa-Form') || aaSubmit.closest('.meili-text-search-form'); 345 if (form) { 346 form.dispatchEvent(new Event('submit', { bubbles: true, cancelable: true })); 347 } 348 return; 349 } 350 351 // Handle outside clicks 352 const clickedInside = 353 aaInput.contains(e.target) || 354 aaPanel.contains(e.target) || 355 (aaSubmit && (aaSubmit.contains(e.target) || e.target.closest('.aa-SubmitButton, .meili-search-submit, button[type="submit"]'))); 356 if (!clickedInside) { 357 aaInput.setAttribute('data-autocomplete-locked', 'true'); 358 aaInput.dispatchEvent(new Event('input', { bubbles: true })); 359 if (autocompleteInstance) { 360 autocompleteInstance.setIsOpen(false); 361 //console.log('Autocomplete: Panel closed via outside click'); 362 } 363 } 364 }); 365 366 document.getElementById('meili-clear-filters')?.addEventListener('click', function () { 367 // Reset in place rather than navigating. The old 368 // `window.location.href = window.location.pathname` dropped every query 369 // param on the URL, including fl_builder while the BB editor was open. 370 const filtersForm = document.getElementById('meili-filters'); 371 if (filtersForm) { 372 filtersForm.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => { 373 cb.checked = false; 374 }); 375 } 376 377 document.querySelectorAll('.meili-text-search-input, .aa-Input').forEach(input => { 378 input.value = ''; 379 }); 380 381 if (typeof loadMeiliResults === 'function') { 382 // Rewrites the URL for us, keeping unrelated params intact. 383 loadMeiliResults('', 1); 384 } else { 385 window.location.href = window.location.pathname; 386 } 387 });
388 389 //console.log('Autocomplete initialized'); 390});
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.