PageSourceSearch

https://kylopeptides.com/wp-content/themes/koi-child/assets/js/koi-search.js?ver=1.1.0

js kylopeptides.com collected 2026-09-28 09:34:00 UTC 4,583 bytes, 137 lines download raw bytes

1/**
2 * koi-search.js — live AJAX search for the Koi header overlay.
3 * Uses the same markup classes as koi-custom.css (.sr-item .sr-img .sr-n .sr-m .sr-stock .sr-p .search-sec .search-empty).
4 * Data comes live from WooCommerce products via admin-ajax.
5 *
6 * Opening the panel now fires an empty query, which the server answers with
7 * popular products. The old version blanked #searchResults on open and bailed
8 * out of runSearch() under two characters, so the overlay was always empty
9 * until you typed.
10 */
11( function () {
12	'use strict';
13
14	var cfg = window.KOI_SEARCH || {};
15	var timer = null;
16	var lastQuery = null;   // null, not '', so the first empty query still runs
17	var defaultHtml = '';   // popular list, cached after the first open
18
19	function el( id ) { return document.getElementById( id ); }
20
21	window.openSearch = function () {
22		var o = el( 'searchOv' );
23		if ( ! o ) { return; }
24		o.classList.add( 'show' );
25		var i = el( 'searchInput' );
26		if ( i ) {
27			i.value = '';
28			setTimeout( function () { i.focus(); }, 40 );
29		}
30		var res = el( 'searchResults' );
31		if ( ! res ) { return; }
32
33		/* Re-use the popular list if we already have it — no spinner, no
34		   round trip on every open. */
35		if ( defaultHtml ) {
36			res.innerHTML = defaultHtml;
37			lastQuery = '';
38		} else {
39			window.runSearch( '' );
40		}
41	};
42
43	window.closeSearch = function () {
44		var o = el( 'searchOv' );
45		if ( o ) { o.classList.remove( 'show' ); }
46	};
47
48	window.runSearch = function ( q ) {
49		q = ( q || '' ).trim();
50		lastQuery = q;
51		var res = el( 'searchResults' );
52		if ( ! res ) { return; }
53
54		/* Box cleared back to empty and we already hold the popular list:
55		   restore it instantly rather than asking the server again. */
56		if ( '' === q && defaultHtml ) {
57			res.innerHTML = defaultHtml;
58			return;
59		}
60
61		if ( timer ) { clearTimeout( timer ); }
62		res.innerHTML = '<div class="search-empty">' + ( q ? 'Searching…' : 'Loading…' ) + '</div>';
63
64		timer = setTimeout( function () {
65			var body = 'action=koi_search&nonce=' + encodeURIComponent( cfg.nonce || '' ) + '&q=' + encodeURIComponent( q );
66			fetch( cfg.ajaxurl, {
67				method: 'POST',
68				headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
69				body: body,
70				credentials: 'same-origin'
71			} )
72			.then( function ( r ) { return r.json(); } )
73			.then( function ( data ) {
74				if ( lastQuery !== q ) { return; } // a newer query already fired
75				renderResults( data, q );
76			} )
77			.catch( function () {
78				res.innerHTML = '<div class="search-empty">Search is unavailable right now.</div>';
79			} );
80		}, q ? 220 : 0 );   // no debounce on the initial open — it isn't typing
81	};
82
83	function esc( s ) {
84		return String( s == null ? '' : s ).replace( /[&<>"']/g, function ( c ) {
85			return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[ c ];
86		} );
87	}
88
89	function renderResults( data, q ) {
90		var res = el( 'searchResults' );
91		if ( ! res ) { return; }
92		data = data || {};
93		var products = data.products || [];
94		var isDefault = !! data.default;
95
96		if ( ! products.length ) {
97			res.innerHTML = q
98				? '<div class="search-empty">No matches for &ldquo;' + esc( q ) + '&rdquo;.</div>'
99				: '<div class="search-empty">No products to show yet.</div>';
100			return;
101		}
102
103		var label = isDefault
104			? 'Popular · ' + products.length
105			: 'Products · ' + products.length;
106
107		var html = '<div class="search-sec">' + label + '</div>';
108		html += products.map( function ( p ) {
109			var stock = p.in_stock
110				? '<span class="sr-stock in">In stock</span>'
111				: '<span class="sr-stock out">Out of stock</span>';
112			var img = p.img
113				? '<div class="sr-img"><img src="' + esc( p.img ) + '" alt=""></div>'
114				: '<div class="sr-img"></div>';
115			return '<a class="sr-item" href="' + esc( p.url ) + '">' +
116				img +
117				'<div><div class="sr-n">' + esc( p.name ) + '</div>' +
118				'<div class="sr-m">' + esc( p.cat || 'Research' ) + '</div></div>' +
119				stock +
120				'<div class="sr-p">' + esc( p.price ) + '</div></a>';
121		} ).join( '' );
122
123		res.innerHTML = html;
124
125		// Keep the popular list around so re-opening is instant.
126		if ( isDefault ) { defaultHtml = html; }
127	}
128
129	// Keyboard: Esc closes, Cmd/Ctrl-K opens.
130	document.addEventListener( 'keydown', function ( e ) {
131		if ( e.key === 'Escape' ) { window.closeSearch(); }
132		if ( ( e.key === 'k' || e.key === 'K' ) && ( e.metaKey || e.ctrlKey ) ) {
133			e.preventDefault();
134			window.openSearch();
135		}
136	} );
137}() );

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.