1/** 2 * Pro-Jazz Live Search 3 * Instant dropdown search with thumbnails and keyboard navigation 4 */ 5(function($) { 6 'use strict'; 7 8 var timer = null; 9 var currentXHR = null; 10 var lastQuery = ''; 11 var $activeInput = null; 12 var $dropdown = null; 13 var activeIndex = -1; 14 15 var icons = { 16 mic: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"></path><path d="M19 10v2a7 7 0 0 1-14 0v-2"></path><line x1="12" y1="19" x2="12" y2="23"></line><line x1="8" y1="23" x2="16" y2="23"></line></svg>', 17 disc: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="3"></circle></svg>', 18 video: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="23 7 16 12 23 17 23 7"></polygon><rect x="1" y="5" width="15" height="14" rx="2" ry="2"></rect></svg>', 19 news: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"></path><path d="M18 14h-8"></path><path d="M15 18h-5"></path><path d="M10 6h8v4h-8V6Z"></path></svg>', 20 tag: '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"></path><line x1="7" y1="7" x2="7.01" y2="7"></line></svg>', 21 note: '<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>' 22 }; 23 24 function initDropdown() { 25 if ($('#live-search-dropdown').length) { 26 $dropdown = $('#live-search-dropdown'); 27 return; 28 } 29 $dropdown = $('<div id="live-search-dropdown" class="live-search-dropdown" style="display:none;"></div>'); 30 $('body').append($dropdown); 31 32 $dropdown.on('mouseenter', '.live-search-item', function() { 33 $dropdown.find('.live-search-item').removeClass('is-selected'); 34 $(this).addClass('is-selected'); 35 activeIndex = parseInt($(this).data('index'), 10); 36 }); 37 } 38 39 function positionDropdown() { 40 if (!$activeInput || !$dropdown || !$dropdown.is(':visible')) return; 41 var offset = $activeInput.offset(); 42 var height = $activeInput.outerHeight(); 43 var width = $activeInput.outerWidth(); 44 var minW = 340; 45 var maxW = 480; 46 var targetW = Math.max(width, minW); 47 if (targetW > maxW && $(window).width() > 600) targetW = maxW; 48 if ($(window).width() <= 480) { 49 targetW = $(window).width() - 20; 50 offset.left = 10; 51 } 52 53 $dropdown.css({ 54 top: (offset.top + height + 4) + 'px', 55 left: offset.left + 'px', 56 width: targetW + 'px' 57 }); 58 } 59 60 function showLoading() { 61 if (!$dropdown) initDropdown(); 62 positionDropdown(); 63 $dropdown.html('<div class="live-search-loading"><span class="live-search-spinner"></span> ÐоиÑк...</div>').show(); 64 if ($activeInput) $activeInput.addClass('live-search-busy'); 65 } 66 67 function hideDropdown() { 68 if ($dropdown) { 69 $dropdown.hide().empty(); 70 } 71 if ($activeInput) { 72 $activeInput.removeClass('live-search-busy'); 73 } 74 activeIndex = -1; 75 } 76 77 function renderResults(data, query) { 78 if (!$dropdown) initDropdown(); 79 if ($activeInput) $activeInput.removeClass('live-search-busy'); 80 81 if (!data || !data.success || !data.categories || data.categories.length === 0) { 82 $dropdown.html('<div class="live-search-empty">Ðо запÑоÑÑ «<b>' + escapeHtml(query) + '</b>» ниÑего не найдено</div>').show(); 83 positionDropdown(); 84 return; 85 } 86 87 var html = '<div class="live-search-content">'; 88 var itemIdx = 0; 89 90 $.each(data.categories, function(ci, cat) { 91 var catIcon = icons[cat.icon] || icons.disc; 92 html += '<div class="live-search-section">'; 93 html += '<div class="live-search-section-header">'; 94 html += '<span class="live-search-section-title"><span class="cat-icon">' + catIcon + '</span> ' + escapeHtml(cat.title) + '</span>'; 95 html += '<a href="' + cat.url + '" class="live-search-section-more">ÐÑе (' + cat.total + ') →</a>'; 96 html += '</div>'; 97 98 html += '<div class="live-search-section-list">'; 99 $.each(cat.items, function(ii, item) { 100 var imgHtml = ''; 101 if (item.image) { 102 imgHtml = '<img src="' + item.image + '" alt="" loading="lazy" class="live-search-thumb" />'; 103 } else { 104 imgHtml = '<div class="live-search-thumb live-search-thumb-empty">' + (icons[cat.icon] || icons.note) + '</div>'; 105 } 106 107 html += '<a href="' + item.url + '" class="live-search-item" data-index="' + itemIdx + '">'; 108 html += imgHtml; 109 html += '<div class="live-search-info">'; 110 html += '<div class="live-search-title">' + item.title_html + '</div>'; 111 if (item.date) { 112 html += '<div class="live-search-meta">' + item.date + '</div>'; 113 } 114 html += '</div>'; 115 html += '</a>'; 116 itemIdx++; 117 }); 118 119 html += '</div>'; 120 html += '</div>'; 121 }); 122 123 html += '</div>'; 124 125 if (data.total > 0 && data.all_url) { 126 html += '<a href="' + data.all_url + '" class="live-search-footer">'; 127 html += 'ÐÑе ÑезÑлÑÑаÑÑ (' + data.total + ') →'; 128 html += '</a>'; 129 } 130 131 $dropdown.html(html).show(); 132 activeIndex = -1; 133 positionDropdown(); 134 } 135 136 function escapeHtml(str) { 137 return $('<div>').text(str || '').html(); 138 } 139 140 function doSearch(query) { 141 if (query === lastQuery && $dropdown && $dropdown.is(':visible')) return; 142 lastQuery = query; 143 144 if (currentXHR) { 145 currentXHR.abort(); 146 currentXHR = null; 147 } 148 149 showLoading(); 150 151 currentXHR = $.ajax({ 152 url: '/search/live', 153 type: 'GET', 154 data: { q: query }, 155 dataType: 'json', 156 success: function(resp) { 157 currentXHR = null;
158 renderResults(resp, query); 159 }, 160 error: function(xhr, status) { 161 if (status === 'abort') return; 162 currentXHR = null; 163 if ($activeInput) $activeInput.removeClass('live-search-busy'); 164 $dropdown.html('<div class="live-search-empty">ÐÑибка пÑи вÑполнении поиÑка</div>').show(); 165 positionDropdown(); 166 } 167 }); 168 } 169 170 $(document).ready(function() { 171 initDropdown(); 172 173 $(document).on('input keyup focus', 'input[name="q"]', function(e) { 174 var $this = $(this); 175 $activeInput = $this; 176 $this.attr('autocomplete', 'off'); 177 178 var val = $.trim($this.val()); 179 180 if (e.type === 'keyup') { 181 if (e.keyCode === 27) { // ESC 182 hideDropdown(); 183 return; 184 } 185 if (e.keyCode === 40) { // DOWN 186 if ($dropdown && $dropdown.is(':visible')) { 187 var items = $dropdown.find('.live-search-item'); 188 if (items.length) { 189 activeIndex = (activeIndex + 1) % items.length; 190 items.removeClass('is-selected'); 191 var $curr = items.eq(activeIndex).addClass('is-selected'); 192 var container = $dropdown.find('.live-search-content'); 193 if (container.length) { 194 container.scrollTop($curr.position().top + container.scrollTop() - 60); 195 } 196 } 197 return false; 198 } 199 } 200 if (e.keyCode === 38) { // UP 201 if ($dropdown && $dropdown.is(':visible')) { 202 var items = $dropdown.find('.live-search-item'); 203 if (items.length) { 204 activeIndex = (activeIndex - 1 + items.length) % items.length; 205 items.removeClass('is-selected'); 206 var $curr = items.eq(activeIndex).addClass('is-selected'); 207 var container = $dropdown.find('.live-search-content'); 208 if (container.length) { 209 container.scrollTop($curr.position().top + container.scrollTop() - 60); 210 } 211 } 212 return false; 213 } 214 } 215 if (e.keyCode === 13) { // ENTER 216 if ($dropdown && $dropdown.is(':visible') && activeIndex >= 0) { 217 var $selected = $dropdown.find('.live-search-item.is-selected'); 218 if ($selected.length) { 219 window.location.href = $selected.attr('href'); 220 return false; 221 } 222 } 223 return true; 224 } 225 } 226 227 if (val.length < 2) { 228 hideDropdown(); 229 lastQuery = ''; 230 return; 231 } 232 233 clearTimeout(timer); 234 timer = setTimeout(function() { 235 doSearch(val); 236 }, 260); 237 }); 238 239 $(document).on('click touchstart', function(e) { 240 if (!$(e.target).closest('#live-search-dropdown, input[name="q"]').length) { 241 hideDropdown(); 242 } 243 }); 244 245 $(window).on('resize scroll', function() { 246 positionDropdown(); 247 }); 248 }); 249 250})(jQuery);
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.