PageSourceSearch

https://popularpet.com.br/wp-content/plugins/ms-smart-search/assets/js/frontend.js?ver=2.1.0

js popularpet.com.br collected 2026-09-24 13:37:49 UTC 16,981 bytes, 497 lines download raw bytes

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, '&amp;')
389                    .replace(/</g, '&lt;')
390                    .replace(/>/g, '&gt;')
391                    .replace(/"/g, '&quot;')
392                    .replace(/'/g, '&#x27;');
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.