1/** 2 * MS Smart Search - Frontend JavaScript 3 * 4 * @package MS_Smart_Search 5 */ 6 7(function($) { 8 'use strict'; 9 10 // Classe principal 11 class MSSmartSearch { 12 constructor(element) { 13 this.$wrapper = $(element); 14 this.$form = this.$wrapper.find('.msss-search-form'); 15 this.$input = this.$wrapper.find('.msss-search-input'); 16 this.$dropdown = this.$wrapper.find('.msss-search-dropdown'); 17 this.$loader = this.$wrapper.find('.msss-search-loader'); 18 this.$icon = this.$wrapper.find('.msss-search-icon'); 19 20 this.searchTimeout = null; 21 this.currentRequest = null; 22 this.selectedIndex = -1; 23 this.results = []; 24 this.history = []; 25 this.lastTerm = ''; 26 this.isOpen = false; 27 28 this.init(); 29 } 30 31 init() { 32 this.bindEvents(); 33 this.loadHistory(); 34 } 35 36 bindEvents() { 37 // Input events 38 this.$input.on('input', this.handleInput.bind(this)); 39 this.$input.on('focus', this.handleFocus.bind(this)); 40 this.$input.on('keydown', this.handleKeydown.bind(this)); 41 42 // Form submit 43 this.$form.on('submit', this.handleSubmit.bind(this)); 44 45 // Click outside to close 46 $(document).on('click', this.handleClickOutside.bind(this)); 47 48 // Dropdown events (delegated) 49 this.$dropdown.on('click', '.msss-clear-history', this.handleClearHistory.bind(this)); 50 this.$dropdown.on('click', '.msss-remove-history-item', this.handleRemoveHistoryItem.bind(this)); 51 this.$dropdown.on('click', '.msss-history-tag', this.handleHistoryTagClick.bind(this)); 52 this.$dropdown.on('click', '.msss-product-card', this.handleProductClick.bind(this)); 53 this.$dropdown.on('click', '.msss-suggestion-item', this.handleSuggestionClick.bind(this)); 54 55 // Prevent dropdown close on click inside 56 this.$dropdown.on('click', function(e) { 57 e.stopPropagation(); 58 }); 59 } 60 61 handleInput(e) { 62 const term = this.$input.val().trim(); 63 64 // Clear timeout 65 if (this.searchTimeout) { 66 clearTimeout(this.searchTimeout); 67 } 68 69 // Cancel current request 70 if (this.currentRequest) { 71 this.currentRequest.abort(); 72 } 73 74 // Check minimum characters 75 if (term.length < msss_params.min_chars) { 76 if (term.length === 0) { 77 this.showHistory(); 78 } else { 79 this.hideDropdown(); 80 } 81 return; 82 } 83 84 // Debounce search 85 this.searchTimeout = setTimeout(() => { 86 this.search(term); 87 }, msss_params.debounce_time); 88 } 89 90 handleFocus(e) { 91 const term = this.$input.val().trim(); 92 93 if (term.length < msss_params.min_chars) { 94 this.showHistory(); 95 } else if (this.results.length > 0) { 96 this.showDropdown(); 97 } 98 } 99 100 handleKeydown(e) { 101 if (!this.isOpen) return; 102 103 const $items = this.$dropdown.find('.msss-suggestion-item, .msss-product-card'); 104 const totalItems = $items.length; 105 106 switch (e.keyCode) { 107 case 40: // Arrow Down 108 e.preventDefault(); 109 this.selectedIndex = Math.min(this.selectedIndex + 1, totalItems - 1); 110 this.highlightItem($items); 111 break; 112 113 case 38: // Arrow Up 114 e.preventDefault(); 115 this.selectedIndex = Math.max(this.selectedIndex - 1, -1); 116 this.highlightItem($items); 117 break; 118 119 case 13: // Enter 120 if (this.selectedIndex >= 0 && this.selectedIndex < totalItems) { 121 e.preventDefault(); 122 const $selected = $items.eq(this.selectedIndex); 123 124 if ($selected.hasClass('msss-suggestion-item')) { 125 const term = $selected.data('term'); 126 window.location.href = msss_params.results_page_url + encodeURIComponent(term) + '&post_type=product'; 127 } else if ($selected.hasClass('msss-product-card')) { 128 window.location.href = $selected.find('a').attr('href'); 129 } 130 } 131 break; 132 133 case 27: // Escape 134 this.hideDropdown(); 135 break; 136 } 137 } 138 139 handleSubmit(e) { 140 const term = this.$input.val().trim(); 141 142 if (term.length >= msss_params.min_chars) { 143 // Track results page search 144 this.trackSearch(term, 'results_page'); 145 } 146 } 147 148 handleClickOutside(e) { 149 if (!this.$wrapper.is(e.target) && this.$wrapper.has(e.target).length === 0) { 150 this.hideDropdown(); 151 } 152 } 153 154 handleClearHistory(e) { 155 e.preventDefault(); 156 e.stopPropagation(); 157 158 $.ajax({ 159 url: msss_params.ajax_url, 160 type: 'POST', 161 data: { 162 action: 'msss_clear_history', 163 nonce: msss_params.nonce 164 }, 165 success: () => { 166 this.history = []; 167 this.hideDropdown(); 168 } 169 }); 170 } 171 172 handleHistoryTagClick(e) { 173 if ($(e.target).closest('.msss-remove-history-item').length) { 174 return; 175 } 176 const term = $(e.currentTarget).data('term'); 177 if (term) { 178 window.location.href = msss_params.results_page_url + encodeURIComponent(term) + '&post_type=product'; 179 } 180 } 181 182 handleRemoveHistoryItem(e) { 183 e.preventDefault(); 184 e.stopPropagation(); 185 186 const $btn = $(e.currentTarget); 187 const term = $btn.data('term'); 188 const $item = $btn.closest('.msss-history-item'); 189 190 $item.fadeOut(150, () => { 191 this.history = this.history.filter(h => h.term !== term); 192 $item.remove(); 193 194 if (this.history.length === 0) { 195 this.hideDropdown(); 196 } 197 }); 198 199 $.ajax({ 200 url: msss_params.ajax_url, 201 type: 'POST', 202 data: { 203 action: 'msss_remove_history_item', 204 nonce: msss_params.nonce, 205 term: term 206 } 207 }); 208 } 209 210 handleProductClick(e) { 211 const $card = $(e.currentTarget); 212 const productId = $card.data('product-id'); 213 const term = this.lastTerm; 214 215 $.ajax({ 216 url: msss_params.ajax_url, 217 type: 'POST', 218 data: { 219 action: 'msss_track_click', 220 nonce: msss_params.nonce, 221 product_id: productId, 222 term: term 223 } 224 }); 225 } 226 227 search(term) { 228 this.lastTerm = term; 229 this.showLoader(); 230 231 this.currentRequest = $.ajax({ 232 url: msss_params.ajax_url, 233 type: 'POST', 234 data: { 235 action: 'msss_search', 236 nonce: msss_params.nonce, 237 term: term, 238 type: 'autocomplete', 239 limit: msss_params.max_results 240 }, 241 success: (response) => { 242 this.hideLoader(); 243 244 if (response.success) { 245 this.results = response.data; 246 this.renderResults(response.data); 247 } 248 }, 249 error: (xhr, status) => { 250 this.hideLoader(); 251 252 if (status !== 'abort') { 253 console.error('Search error:', status); 254 } 255 } 256 }); 257 } 258 259 handleSuggestionClick(e) { 260 const term = $(e.currentTarget).data('term'); 261 window.location.href = msss_params.results_page_url + encodeURIComponent(term) + '&post_type=product'; 262 } 263 264 trackSearch(term, type) { 265 $.ajax({ 266 url: msss_params.ajax_url, 267 type: 'POST', 268 data: { 269 action: 'msss_search', 270 nonce: msss_params.nonce, 271 term: term, 272 type: type, 273 limit: 1 274 } 275 }); 276 } 277 278 loadHistory() { 279 if (!msss_params.show_history) return; 280 281 $.ajax({ 282 url: msss_params.ajax_url, 283 type: 'POST', 284 data: { 285 action: 'msss_get_history', 286 nonce: msss_params.nonce, 287 limit: 10 288 }, 289 success: (response) => { 290 if (response.success) { 291 this.history = response.data; 292 } 293 } 294 }); 295 } 296 297 renderResults(data) { 298 const templateHtml = $('#msss-results-template').html(); 299 300 if (!templateHtml) { 301 console.error('Template not found'); 302 return; 303 } 304 305 const templateData = { 306 products: data.products || [], 307 categories: msss_params.show_categories ? (data.categories || []) : [], 308 textSuggestions: data.text_suggestions || [], 309 history: [], 310 total: data.total || 0, 311 term: data.term || '', 312 minChars: msss_params.min_chars, 313 maxResults: msss_params.max_results, 314 showSku: msss_params.show_sku, 315 currencySymbol: msss_params.currency_symbol, 316 resultsPageUrl: msss_params.results_page_url, 317 i18n: msss_params.i18n, 318 was_corrected: data.was_corrected || false, 319 corrected_to: data.corrected_to || '', 320 suggestions: data.suggestions || [] 321 }; 322 323 // Highlight search term in product names (garante que name é string) 324 if (templateData.products.length > 0 && data.term) { 325 const searchTerm = data.was_corrected ? data.corrected_to : data.term; 326 const regex = new RegExp('(' + this.escapeRegex(searchTerm) + ')', 'gi'); 327 templateData.products = templateData.products.map(product => { 328 const name = String(product.name || ''); 329 return { 330 ...product, 331 name: name.replace(regex, '<span class="msss-highlight">$1</span>') 332 }; 333 }); 334 } 335 336 const html = this.renderTemplate(templateHtml, templateData); 337 this.$dropdown.html(html); 338 this.showDropdown(); 339 this.selectedIndex = -1; 340 } 341 342 showHistory() { 343 if (!msss_params.show_history || this.history.length === 0) { 344 this.hideDropdown(); 345 return; 346 } 347 348 const templateHtml = $('#msss-results-template').html(); 349 350 if (!templateHtml) return; 351 352 const templateData = { 353 products: [], 354 categories: [], 355 textSuggestions: [], 356 suggestions: [], 357 history: this.history, 358 total: 0, 359 term: '', 360 minChars: msss_params.min_chars, 361 maxResults: msss_params.max_results, 362 showSku: msss_params.show_sku, 363 resultsPageUrl: msss_params.results_page_url, 364 i18n: msss_params.i18n, 365 was_corrected: false, 366 corrected_to: '' 367 }; 368 369 const html = this.renderTemplate(templateHtml, templateData); 370 this.$dropdown.html(html); 371 this.showDropdown(); 372 } 373 374 renderTemplate(templateStr, data) { 375 // Use Underscore.js-style template compilation 376 // Converts template to JavaScript function for proper execution 377 378 const settings = { 379 evaluate: /<%([\s\S]+?)%>/g, 380 interpolate: /<%=([\s\S]+?)%>/g, 381 escape: /<%-([\s\S]+?)%>/g 382 }; 383 384 // HTML entity escape function 385 const escapeHTML = (str) => { 386 if (str == null) return ''; 387 return String(str) 388 .replace(/&/g, '&') 389 .replace(/</g, '<') 390 .replace(/>/g, '>') 391 .replace(/"/g, '"') 392 .replace(/'/g, '''); 393 }; 394 395 // Build the function body 396 let index = 0; 397 let source = "__p+='"; 398 399 // Combine patterns 400 const matcher = new RegExp([ 401 settings.interpolate.source, 402 settings.evaluate.source 403 ].join('|') + '|$', 'g'); 404 405 templateStr.replace(matcher, function(match, interpolate, evaluate, offset) { 406 // Add the text before the match (with escaping for JS string) 407 source += templateStr.slice(index, offset) 408 .replace(/\\/g, '\\\\') 409 .replace(/'/g, "\\'") 410 .replace(/\r/g, '\\r') 411 .replace(/\n/g, '\\n') 412 .replace(/\t/g, '\\t'); 413 414 index = offset + match.length; 415 416 if (interpolate) { 417 // <%= ... %> - interpolate 418 source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'";
419 } else if (evaluate) { 420 // <% ... %> - evaluate 421 source += "';\n" + evaluate + "\n__p+='"; 422 } 423 424 return match; 425 }); 426 427 source += "';\n"; 428 429 // Wrap in function 430 source = 'var __t,__p="",__j=Array.prototype.join;' + 431 'function print(){__p+=__j.call(arguments,"");}' + 432 'with(obj||{}){\n' + source + '}\nreturn __p;\n'; 433 434 try { 435 const render = new Function('obj', 'escapeHTML', source); 436 return render.call(data, data, escapeHTML); 437 } catch (e) { 438 console.error('Template compile error:', e.message); 439 console.error('Source:', source); 440 return ''; 441 } 442 } 443 444 highlightItem($items) { 445 $items.removeClass('active'); 446 447 if (this.selectedIndex >= 0 && this.selectedIndex < $items.length) { 448 const $selected = $items.eq(this.selectedIndex); 449 $selected.addClass('active'); 450 451 // Scroll into view 452 const dropdown = this.$dropdown[0]; 453 const item = $selected[0]; 454 455 if (item.offsetTop < dropdown.scrollTop) { 456 dropdown.scrollTop = item.offsetTop; 457 } else if (item.offsetTop + item.offsetHeight > dropdown.scrollTop + dropdown.offsetHeight) { 458 dropdown.scrollTop = item.offsetTop + item.offsetHeight - dropdown.offsetHeight; 459 } 460 } 461 } 462 463 showDropdown() { 464 this.$dropdown.show(); 465 this.isOpen = true; 466 } 467 468 hideDropdown() { 469 this.$dropdown.hide(); 470 this.isOpen = false; 471 this.selectedIndex = -1; 472 } 473 474 showLoader() { 475 this.$wrapper.addClass('is-loading'); 476 } 477 478 hideLoader() { 479 this.$wrapper.removeClass('is-loading'); 480 } 481 482 escapeRegex(string) { 483 return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); 484 } 485 } 486 487 // Initialize on document ready 488 $(document).ready(function() { 489 $('.msss-search-wrapper').each(function() { 490 new MSSmartSearch(this); 491 }); 492 }); 493 494 // Expose to global for external use 495 window.MSSmartSearch = MSSmartSearch; 496 497})(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.