PageSourceSearch

https://www.whoi.edu/wp-content/plugins/whoi-meili-search-sync/front/assets/js/meili-autocomplete.js?ver=1.0

js whoi.edu collected 2026-10-01 08:39:39 UTC 18,866 bytes, 390 lines download raw bytes

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.