PageSourceSearch

https://www.justborn.com/wp-content/themes/studioswift/assets/js/frontend/search.js

js justborn.com collected 2026-10-02 08:00:52 UTC 7,206 bytes, 279 lines download raw bytes

1/**
2 * Site Search Functionality
3 *
4 * Handles AJAX search with debouncing and result display.
5 *
6 * @package StudioSwift
7 */
8
9(function() {
10	'use strict';
11
12	// Wait for DOM to be ready.
13	document.addEventListener('DOMContentLoaded', function() {
14		initSearch();
15	});
16
17	/**
18	 * Initialize search functionality
19	 */
20	function initSearch() {
21		const searchModal = document.getElementById('site-search');
22		const searchInput = document.getElementById('site-search-input');
23		const searchResults = document.querySelector('.site-search__results ul');
24		const searchFooter = document.querySelector('.site-search__footer');
25		const clearButton = document.getElementById('site-search-clear');
26
27		if (!searchModal || !searchInput || !searchResults) return;
28
29		const resultsContainer = searchResults.closest('.site-search__results');
30
31		let searchTimeout;
32		let currentRequest;
33
34		// Clear results initially and hide footer
35		clearResults();
36		if (searchFooter) {
37			searchFooter.style.display = 'none';
38		}
39
40		// Handle keyup with debouncing
41		searchInput.addEventListener('keyup', function(e) {
42			// Skip if it's just navigation keys
43			if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Tab', 'Shift', 'Control', 'Alt', 'Meta'].includes(e.key)) {
44				return;
45			}
46
47			const query = this.value.trim();
48
49			// Clear previous timeout
50			if (searchTimeout) {
51				clearTimeout(searchTimeout);
52			}
53
54			// Cancel previous request
55			if (currentRequest) {
56				currentRequest.abort();
57			}
58
59			if (query.length === 0) {
60				clearResults();
61				if (searchFooter) {
62					searchFooter.style.display = 'none';
63				}
64				return;
65			}
66
67			// Don't show loading immediately, wait for the timeout
68			// Debounce search - wait 1.5 seconds after user stops typing
69			searchTimeout = setTimeout(function() {
70				showLoading();
71				performSearch(query);
72			}, 700);
73		});
74
75		// Handle Enter key to perform immediate search
76		searchInput.addEventListener('keydown', function(e) {
77			if (e.key === 'Enter') {
78				e.preventDefault();
79				const query = this.value.trim();
80				
81				if (query.length > 0) {
82					// Clear timeout and perform immediate search
83					if (searchTimeout) {
84						clearTimeout(searchTimeout);
85					}
86					
87					// Cancel previous request
88					if (currentRequest) {
89						currentRequest.abort();
90					}
91					
92					showLoading();
93					performSearch(query);
94				}
95			}
96		});
97
98		// Clear button functionality
99		if (clearButton) {
100			clearButton.addEventListener('click', function() {
101				searchInput.value = '';
102				clearResults();
103				if (searchFooter) {
104					searchFooter.style.display = 'none';
105				}
106				searchInput.focus();
107			});
108		}
109
110		/**
111		 * Perform AJAX search
112		 */
113		function performSearch(query) {
114			if (!window.studioswiftData) {
115				console.error('StudioSwift: AJAX data not available');
116				return;
117			}
118
119			const formData = new FormData();
120			formData.append('action', 'studioswift_search');
121			formData.append('query', query);
122			formData.append('nonce', window.studioswiftData.nonce);
123
124			currentRequest = new XMLHttpRequest();
125			currentRequest.open('POST', window.studioswiftData.ajaxUrl);
126
127			currentRequest.onload = function() {
128				if (this.status === 200) {
129					try {
130						const response = JSON.parse(this.responseText);
131						if (response.success) {
132							displayResults(response.data, query);
133						} else {
134							showError('Search failed. Please try again.');
135						}
136					} catch (e) {
137						showError('Search failed. Please try again.');
138					}
139				} else {
140					showError('Search failed. Please try again.');
141				}
142				currentRequest = null;
143			};
144
145			currentRequest.onerror = function() {
146				showError('Search failed. Please try again.');
147				currentRequest = null;
148			};
149
150			currentRequest.send(formData);
151		}
152
153		/**
154		 * Display search results
155		 */
156		function displayResults(data, query) {
157			if (data.results && data.results.length > 0) {
158				// Show results
159				let html = '';
160				data.results.forEach(function(result) {
161					html += `
162						<li>
163							<div class="ss search-result">
164								<a class="l-link" href="${result.permalink}" role="menuitem">Read more</a>
165								<figure>
166									<img src="${result.thumbnail}" 
167										 width="135" height="135" alt="${escapeHtml(result.title)}">
168								</figure>
169								<article>
170									<h6>${escapeHtml(result.title)}</h6>
171									<p class="search-result__desc">${escapeHtml(result.post_type)}</p>
172								</article>
173							</div>
174						</li>
175					`;
176				});
177
178				searchResults.innerHTML = html;
179
180				// Announce results to screen readers
181				const resultsContainer = searchResults.closest('.site-search__results');
182				if (resultsContainer) {
183					resultsContainer.setAttribute('aria-label', `${data.results.length} search results found for ${query}`);
184				}
185
186			// Show footer with "View all results" link
187			if (searchFooter) {
188				const footerLink = searchFooter.querySelector('a');
189				const footerText = searchFooter.querySelector('.js-search-footer-text');
190				
191				if (footerLink && footerText) {
192					// Update only the text span, leaving the icon intact
193					footerText.textContent = `View all results for "${query}"`;
194					footerLink.href = `${window.studioswiftData.siteUrl}?s=${encodeURIComponent(query)}`;
195					footerLink.setAttribute('title', `View all results for ${escapeHtml(query)}`);
196				}
197				searchFooter.style.display = 'block';
198			}
199			} else {
200				// Show no results message
201				const message = data.message || `No results found for "${query}".`;
202				searchResults.innerHTML = `
203					<li>
204						<div class="ss search-result search-result--no-results">
205							<h4>${escapeHtml(message)}</h4>
206							<p>Check the spelling or use a different word or phrase.</p>
207						</div>
208					</li>
209				`;
210
211				// Announce no results to screen readers
212				const resultsContainer = searchResults.closest('.site-search__results');
213				if (resultsContainer) {
214					resultsContainer.setAttribute('aria-label', `No search results found for ${query}.Check the spelling or use a different word or phrase.`);
215				}
216
217				if (searchFooter) {
218					searchFooter.style.display = 'none';
219				}
220			}
221		}
222
223		/**
224		 * Show loading state
225		 */
226		function showLoading() {
227			if (resultsContainer) resultsContainer.style.display = '';
228			searchResults.innerHTML = `
229				<li>
230					<div class="ss search-result search-result--loading">
231						<div class="search-loader">
232							<div class="search-loader__spinner"></div>
233							<p>Searching...</p>
234						</div>
235					</div>
236				</li>
237			`;
238
239			if (searchFooter) {
240				searchFooter.style.display = 'none';
241			}
242		}
243
244		/**
245		 * Show error message
246		 */
247		function showError(message) {
248			searchResults.innerHTML = `
249				<li>
250					<div class="ss search-result search-result--error">
251						<p>${escapeHtml(message)}</p>
252					</div>
253				</li>
254			`;
255
256			if (searchFooter) {
257				searchFooter.style.display = 'none';
258			}
259		}
260
261		/**
262		 * Clear search results
263		 */
264		function clearResults() {
265			searchResults.innerHTML = '';
266			if (resultsContainer) resultsContainer.style.display = 'none';
267		}
268
269		/**
270		 * Escape HTML to prevent XSS
271		 */
272		function escapeHtml(text) {
273			const div = document.createElement('div');
274			div.textContent = text;
275			return div.innerHTML;
276		}
277	}
278
279})();

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.