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 { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ 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 “' + esc( q ) + '”.</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.