PageSourceSearch

https://xeople.com/wp-content/themes/xeople_dark_theme/js/live-search.js

js xeople.com collected 2026-10-09 04:43:08 UTC 9,417 bytes, 290 lines download raw bytes

1/**
2 * ============================================================
3 * LIVE SEARCH v2.0 — Frontend JavaScript (FIXED)
4 * ============================================================
5 * 
6 * Fixed issues:
7 *  - Works with both #ls-input and .ls-search-input selectors
8 *  - Works with #ls-dropdown for both popup and search page
9 *  - Proper form handling with id="ls-form"
10 *  - Autocomplete dropdown suggestions
11 *  - Keyboard navigation (arrow keys, enter, escape)
12 *  - Click outside to close
13 * ============================================================
14 */
15
16(function() {
17    'use strict';
18
19    const liveSearch = {
20        // DOM elements
21        input: null,
22        dropdown: null,
23        form: null,
24
25        // State
26        isOpen: false,
27        debounceTimer: null,
28        selectedIndex: -1,
29        currentKeyword: '',
30
31        /**
32         * Initialize live search
33         */
34        init: function() {
35            // Support both #ls-input (popup/search page) and .ls-search-input (custom class)
36            this.input = document.querySelector('#ls-input') || document.querySelector('.ls-search-input');
37            this.dropdown = document.querySelector('#ls-dropdown') || document.querySelector('.ls-dropdown');
38            this.form = document.querySelector('#ls-form') || document.querySelector('.ls-search-form');
39
40            if ( ! this.input || ! this.dropdown ) {
41                console.warn('Live Search: Missing required elements (input or dropdown)');
42                return;
43            }
44
45            // Bind events
46            this.input.addEventListener('input', (e) => this.onInput(e));
47            this.input.addEventListener('keydown', (e) => this.onKeyDown(e));
48            this.input.addEventListener('focus', (e) => this.onFocus(e));
49            document.addEventListener('click', (e) => this.onDocumentClick(e));
50
51            // Submit form with Enter key
52            if ( this.form ) {
53                this.form.addEventListener('submit', (e) => this.onFormSubmit(e));
54            }
55
56            console.log('Live Search: Initialized successfully');
57        },
58
59        /**
60         * Handle input event
61         */
62        onInput: function(e) {
63            const keyword = e.target.value.trim();
64
65            // Clear previous debounce
66            if ( this.debounceTimer ) {
67                clearTimeout(this.debounceTimer);
68            }
69
70            // If keyword is too short, close dropdown
71            if ( keyword.length < liveSearchData.minChars ) {
72                this.close();
73                return;
74            }
75
76            // Debounce the AJAX call
77            this.debounceTimer = setTimeout(() => {
78                this.fetchSuggestions(keyword);
79            }, 300);
80        },
81
82        /**
83         * Handle keyboard navigation
84         */
85        onKeyDown: function(e) {
86            const items = this.dropdown.querySelectorAll('.ls-suggestion-item');
87            
88            switch(e.key) {
89                case 'ArrowDown':
90                    e.preventDefault();
91                    if ( ! this.isOpen ) return;
92                    
93                    if ( this.selectedIndex < items.length - 1 ) {
94                        this.selectedIndex++;
95                        this.updateSelected(items);
96                    }
97                    break;
98
99                case 'ArrowUp':
100                    e.preventDefault();
101                    if ( ! this.isOpen ) return;
102                    
103                    if ( this.selectedIndex > 0 ) {
104                        this.selectedIndex--;
105                        this.updateSelected(items);
106                    }
107                    break;
108
109                case 'Enter':
110                    e.preventDefault();
111                    // Always navigate to full results page
112                    this.submitSearch();
113                    break;
114
115                case 'Escape':
116                    e.preventDefault();
117                    this.close();
118                    break;
119            }
120        },
121
122        /**
123         * Handle focus event
124         */
125        onFocus: function(e) {
126            const keyword = e.target.value.trim();
127            if ( keyword.length >= liveSearchData.minChars && this.dropdown.innerHTML.trim() ) {
128                this.open();
129            }
130        },
131
132        /**
133         * Handle document click (close dropdown if clicking outside)
134         */
135        onDocumentClick: function(e) {
136            // Close if clicking outside input and dropdown
137            const isClickInside = this.input.contains(e.target) || this.dropdown.contains(e.target);
138            if ( ! isClickInside && this.isOpen ) {
139                this.close();
140            }
141        },
142
143        /**
144         * Handle form submission
145         */
146        onFormSubmit: function(e) {
147            e.preventDefault();
148            this.submitSearch();
149        },
150
151        /**
152         * Submit search and navigate to results page
153         */
154        submitSearch: function() {
155            const keyword = this.input.value.trim();
156            
157            if ( keyword.length < liveSearchData.minChars ) {
158                this.input.focus();
159                return;
160            }
161
162            // Navigate to search results page with keyword
163            const url = liveSearchData.resultsPageUrl + encodeURIComponent(keyword);
164            window.location.href = url;
165        },
166
167        /**
168         * Fetch autocomplete suggestions via AJAX
169         */
170        fetchSuggestions: function(keyword) {
171            this.currentKeyword = keyword;
172
173            const formData = new FormData();
174            formData.append('action', liveSearchData.action);
175            formData.append('nonce', liveSearchData.nonce);
176            formData.append('keyword', keyword);
177
178            fetch(liveSearchData.ajaxUrl, {
179                method: 'POST',
180                body: formData
181            })
182            .then(response => {
183                if ( ! response.ok ) {
184                    throw new Error('Network response was not ok');
185                }
186                return response.text();
187            })
188            .then(html => {
189                // Update dropdown with suggestions
190                if ( html.trim() ) {
191                    this.dropdown.innerHTML = html;
192                    this.selectedIndex = -1;
193                    this.open();
194                    
195                    // Bind click handlers to suggestion items
196                    this.bindSuggestionClicks();
197                } else {
198                    this.close();
199                }
200            })
201            .catch(error => {
202                console.error('Live search error:', error);
203                this.close();
204            });
205        },
206
207        /**
208         * Bind click handlers to suggestion items
209         */
210        bindSuggestionClicks: function() {
211            const items = this.dropdown.querySelectorAll('.ls-suggestion-item');
212            
213            items.forEach((item, index) => {
214                item.addEventListener('click', (e) => {
215                    e.preventDefault();
216                    e.stopPropagation();
217                    
218                    // Set the input to the suggestion text
219                    const textSpan = item.querySelector('.ls-suggestion-text');
220                    if ( textSpan ) {
221                        this.input.value = textSpan.textContent.trim();
222                    }
223                    this.close();
224                    // Navigate to full results page
225                    this.submitSearch();
226                });
227
228                item.addEventListener('mouseenter', () => {
229                    this.selectedIndex = index;
230                    this.updateSelected(items);
231                });
232            });
233
234            // Also bind the "View all results" footer link
235            const footerLink = this.dropdown.querySelector('.ls-see-all-link');
236            if ( footerLink ) {
237                footerLink.addEventListener('click', (e) => {
238                    e.preventDefault();
239                    const url = footerLink.getAttribute('href');
240                    window.location.href = url;
241                });
242            }
243        },
244
245        /**
246         * Update visual selection
247         */
248        updateSelected: function(items) {
249            items.forEach((item, index) => {
250                if ( index === this.selectedIndex ) {
251                    item.classList.add('selected');
252                    item.scrollIntoView({ block: 'nearest' });
253                } else {
254                    item.classList.remove('selected');
255                }
256            });
257        },
258
259        /**
260         * Open dropdown
261         */
262        open: function() {
263            if ( ! this.dropdown.innerHTML.trim() ) {
264                return;
265            }
266            
267            this.isOpen = true;
268            this.dropdown.style.display = 'block';
269        },
270
271        /**
272         * Close dropdown
273         */
274        close: function() {
275            this.isOpen = false;
276            this.dropdown.style.display = 'none';
277            this.selectedIndex = -1;
278        }
279    };
280
281    // Initialize when DOM is ready
282    if ( document.readyState === 'loading' ) {
283        document.addEventListener('DOMContentLoaded', () => liveSearch.init());
284    } else {
285        liveSearch.init();
286    }
287
288    // Expose to window for debugging
289    window.liveSearch = liveSearch;
290})();

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.