PageSourceSearch

https://markformelle.kz/local/components/markformelle/mf.search.au…/header_any/script.js?177935438742113

js markformelle.kz collected 2026-09-28 22:49:09 UTC 42,113 bytes, 884 lines download raw bytes

1$(document).ready(function () {
2    var ACTIVE_ITEM_CLASS = 'app-desktop-search__popup-link_active';
3
4    /* ------------------------------------------------------------------ */
5    /* Открытие / закрытие блока автодополнения                           */
6    /* ------------------------------------------------------------------ */
7
8    function openSearchPopup() {
9        $('.js-search-ac-body').addClass('is-open');
10    }
11
12    function closeSearchPopup() {
13        var $acBody = $('.js-search-ac-body');
14        // Скрываем блок автодополнения сразу, чтобы не было "половины" контента.
15        $acBody.removeClass('is-open');
16        // console.log('closeSearchPopup');
17        $('.app-desktop-search__input-wrap').removeClass('has-value');
18        clearActiveItem();
19        renderSuggestions([]);
20
21        // Полное закрытие поиска в шапке без программного click()
22        // (иначе получаем рекурсию обработчиков и stack overflow).
23        var $desktopSearchBtn = $('.app-desktop-search-btn');
24        // Запускаем обратную анимацию снизу вверх.
25        $desktopSearchBtn.addClass('for-close search-popup-animation search-form-active');
26        $('.app-search-backdrop').remove();
27        $('body').removeClass('search-popup-is-opening');
28
29        setTimeout(function () {
30            $desktopSearchBtn.removeClass('for-close search-popup-active search-popup-animation search-form-active');
31        }, 600);
32
33        if ($(window).scrollTop() < 1) {
34            $('body').addClass('app-scroll-pos-top');
35        }
36    }
37
38    /* ------------------------------------------------------------------ */
39    /* Активный элемент (навигация стрелками)                              */
40    /* ------------------------------------------------------------------ */
41
42    function clearActiveItem() {
43        $('.app-desktop-search__popup-link').removeClass(ACTIVE_ITEM_CLASS);
44    }
45
46    function setActiveItemByIndex(index) {
47        var $items = $('.app-desktop-search__popup-link');
48        clearActiveItem();
49
50        if (!$items.length) return;
51
52        if (index < 0) index = $items.length - 1;
53        else if (index >= $items.length) index = 0;
54
55        $items.eq(index).addClass(ACTIVE_ITEM_CLASS);
56    }
57
58    function getActiveItemIndex() {
59        var activeIndex = -1;
60        $('.app-desktop-search__popup-link').each(function (i) {
61            if ($(this).hasClass(ACTIVE_ITEM_CLASS)) {
62                activeIndex = i;
63                return false;
64            }
65        });
66        return activeIndex;
67    }
68
69    /* ------------------------------------------------------------------ */
70    /* Рендер подсказок (текстовые запросы)                                */
71    /* ------------------------------------------------------------------ */
72
73    var HISTORY_KEY = 'mf_search_history';
74    var MAX_HISTORY = 3;
75    var MAX_SUGGESTIONS = 3;
76    var lastCorrection = '';
77
78    function getHistory() {
79        try {
80            return JSON.parse(localStorage.getItem(HISTORY_KEY) || '[]');
81        } catch (e) {
82            return [];
83        }
84    }
85
86    function saveToHistory(query) {
87        if (!query) return;
88        var history = getHistory().filter(function (q) { return q !== query; });
89        history.unshift(query);
90        if (history.length > MAX_HISTORY) history = history.slice(0, MAX_HISTORY);
91        localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
92    }
93
94    function clearHistory() {
95        localStorage.removeItem(HISTORY_KEY);
96    }
97
98    function buildQueryIcon(type) {
99        if (type === 'history') {
100            return '<svg class="app-desktop-search__popup-link-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M8 1.5A6.5 6.5 0 1 0 14.5 8 6.508 6.508 0 0 0 8 1.5ZM8 13A5 5 0 1 1 13 8 5.006 5.006 0 0 1 8 13Zm.75-8.25h-1.5v3.44l2.28 2.28 1.06-1.06-1.84-1.84Z" fill="currentColor"/></svg>';
101        }
102        if (type === 'category') {
103            return '<svg class="app-desktop-search__popup-link-icon" viewB
103ox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="1" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="1" y="9" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.2"/><rect x="9" y="9" width="6" height="6" rx="1" stroke="currentColor" stroke-width="1.2"/></svg>';
104        }
105        return '<svg class="app-desktop-search__popup-link-icon" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.8 1.4a5.4 5.4 0 1 0 0 10.8A5.4 5.4 0 0 0 6.8 1.4ZM0 6.8a6.8 6.8 0 1 1 13.6 0A6.8 6.8 0 0 1 0 6.8Z" fill="currentColor"/><path fill-rule="evenodd" clip-rule="evenodd" d="M10.415 10.415a.7.7 0 0 1 .99 0l2.88 2.88a.7.7 0 1 1-.99.99l-2.88-2.88a.7.7 0 0 1 0-.99Z" fill="currentColor"/></svg>';
106    }
107
108    function buildLinkHtml(text, type, href) {
109        var url = href || ('/search/?q=' + encodeURIComponent(text));
110        return '<a class="app-desktop-search__popup-link" href="' + url + '">'
111            + buildQueryIcon(type)
112            + '<span>' + escapeHtml(text) + '</span>'
113            + '</a>';
114    }
115
116    function renderSuggestions(suggestions) {
117        var $historyBlock = $('.js-search-history-block');
118        var $popularBlock = $('.js-search-popular-block');
119        var $historyList  = $('.js-search-history-list');
120        var $popularList  = $('.js-search-popular-list');
121
122        var history = getHistory();
123
124        /* История */
125        if (history.length) {
126            $historyList.html(history.map(function (q) { return buildLinkHtml(q, 'history'); }).join(''));
127            $historyBlock.show();
128        } else {
129            $historyList.html('');
130            $historyBlock.hide();
131        }
132
133        /* Подсказки от сервера */
134        if (suggestions.length) {
135            $popularList.html(suggestions.slice(0, MAX_SUGGESTIONS).map(function (q) { return buildLinkHtml(q, 'search'); }).join(''));
136            $popularBlock.show();
137        } else {
138            $popularList.html('');
139            $popularBlock.hide();
140        }
141
142        /* Видимость попапа управляется централизованно в updateAutocompleteVisibility() */
143    }
144
145    /* ------------------------------------------------------------------ */
146    /* Рендер категорий                                                    */
147    /* ------------------------------------------------------------------ */
148
149    function renderCategories(categories) {
150        var $block = $('.js-search-categories-block');
151        var $list  = $('.js-search-categories-list');
152
153        if (!categories || !categories.length) {
154            $list.html('');
155            $block.hide();
156            return;
157        }
158
159        $list.html(
160            categories.slice(0, MAX_SUGGESTIONS).map(function (cat) {
161                return buildLinkHtml(cat.name || cat, 'category', cat.url || null);
162            }).join('')
163        );
164        $block.show();
165    }
166
167    /* ------------------------------------------------------------------ */
168    /* Рендер табов-уточнений                                              */
169    /* ------------------------------------------------------------------ */
170
171    function renderTaps(taps) {
172        var $tapsContainer = $('.app-desktop-search__taps');
173        if (!taps || !taps.length) {
174            $tapsContainer.html('');
175            $tapsContainer.hide();
176            return;
177        }
178        $tapsContainer.html(
179            taps.map(function (tap) {
180                return '<button type="button" class="app-desktop-search__tap">'
181                    + escapeHtml(tap)
182                    + '</button>';
183            }).join('')
184        );
185        $tapsContainer.show();
186    }
187
188    /* Клик по тапу-уточнению: при correction заменяем последнее слово, иначе дописываем */
189    $(document).on('click', '.app-desktop-search__tap', function () {
190        var $input = $('#header-search-input');
191        if (!$input.length) {
192            return;
193        }
194
195        var current = $input.val() || '';
196        var tapText = $(this).text() || '';
197        tapText = tapText.trim();
198        if (!tapText) {
199            return;
200        }
201
202        var newValue = '';
203        if (lastCorrection !== '') {
204            var currentTrimmedRight = String(current).replace(/\s+$/, '');
205            if (currentTrimmedRight === '') {
206                newValue = tapText;
207            } else if (/\S/.test(currentTrimmedRight)) {
208                newValue = currentTrimmedRight.replace(/\S+$/, tapText);
209            } else {
210                newValue = tapText;
211            }
212        } else {
213            var needsSpace = current.length > 0 && !/\s$/.test(current);
214            newValue = current + (needsSpace ? ' ' : '') + tapText;
215        }
216
217        $input.val(newValue).trigger('input');
218        $input.focus();
219    });
220
221    /* ------------------------------------------------------------------ */
222    /* Рендер карточек товаров                                             */
223    /* ------------------------------------------------------------------ */
224
225    function buildProductCard(item) {
226        function normalizePriceSpacing(value) {
227            return String(value)
228                .replace(/&nbsp;/g, '\u00A0')
229                .replace(/&#160;/g, '\u00A0');
230        }
231
232        function offerIsOnlyInStore(offer) {
233            return offer.only_in_store === true || offer.only_in_store === 1
234                || offer.ONLY_IN_STORE === '1' || offer.ONLY_IN_STORE === 1;
235        }
236
237        /* Плашка скидки */
238        var badge = item.discount
239            ? '<span class="app-desktop-search__product-badge">' + escapeHtml(item.discount) + '</span>'
240            : '';
241
242        /* Метки (хит продаж, новинка и т.п.) */
243        var labels = (item.labels && item.labels.length)
244            ? '<div class="app-desktop-search__product-labels">'
245                + item.labels.map(function (l) { return '<span class="app-desktop-search__product-label">' + escapeHtml(l) + '</span>'; }).join('')
246                + '</div>'
247            : '';
248
249        /* Рейтинг-звёзды */
250        var stars = (item.rating !== null && item.rating !== undefined)
251            ? '<div class="app-desktop-search__product-stars">' + buildStars(item.rating) + '</div>'
252            : '';
253
254        /* Строка мета (метки + звёзды).
255         * Обёртка выводится всегда, чтобы высота карточек была одинаковой
256         * даже при отсутствии рейтинга и меток.
257         */
258        var meta = '<div class="app-desktop-search__product-meta">' + labels + stars + '</div>';
259
260        /* Текущая цена */
261        var hasFormattedPrice = item.price_formatted !== null && item.price_formatted !== undefined && String(item.price_formatted).trim() !== '';
262        var currencyOld = item.currency ? ' ' + escapeHtml(item.currency) : '';
263        var currency = hasFormattedPrice ? '' : currencyOld;
264        var priceDisplay = hasFormattedPrice ? normalizePriceSpacing(item.price_formatted) : item.price;
265        var price = (priceDisplay !== null && priceDisplay !== undefined)
266            ? '<span class="app-desktop-search__product-price">' + escapeHtml(String(priceDisplay)) + currency + '</span>'
267            : '';
268
269        /* Старая цена */
270        var showOldPrice = item.price_old && item.price_old > item.price;
271        var priceOldDisplay = (item.price_old_formatted !== null && item.price_old_formatted !== undefined && String(item.price_old_formatted).trim() !== '')
272            ? normalizePriceSpacing(item.price_old_formatted)
273            : item.price_old;
274        var currencyOldForOld = (item.price_old_formatted !== null && item.price_old_formatted !== undefined && String(item.price_old_formatted).trim() !== '')
275            ? ''
276            : currencyOld;
277        var priceOld = showOldPrice
278            ? '<span class="app-desktop-search__product-price-old">' + escapeHtml(String(priceOldDisplay)) + currencyOldForOld + '</span>'
279            : '';
280
281        /* Блок цен */
282        var prices = (price || priceOld)
283            ? '<div class="app-desktop-search__product-prices">' + price + priceOld + '</div>'
284            : '';
285
286        /* Счётчик цветов */
287        var colorsCount = item.colors ? parseInt(item.colors, 10) : 0;
288        var colorsText = colorsCount > 0 ? colorsCount + ' ' + declOfNum(colorsCount, ['цвет', 'цвета', 'цветов']) : '';
289        var colorsHtml = colorsText
290            ? '<span class="app-desktop-search__product-colors">' + escapeHtml(colorsText) + '</span>'
291            : '';
292
293        /* Кнопка избранного — использует глобаÐ
293»ÑŒÐ½Ñ‹Ð¹ обработчик add-in-favorite-button */
294        var favActiveClass = item.in_fav ? ' active' : '';
295        var favBtn = item.favorite_offer_id
296            ? '<button type="button" class="add-in-favorite-button app-desktop-search__fav-btn' + favActiveClass + '" data-item="' + item.favorite_offer_id + '" data-remove-block="Y" aria-label="В избранное">'
297                + '<svg class="white_heart" width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.6301 3.4575C15.247 3.07425 14.7922 2.77023 14.2916 2.56281C13.791 2.35539 13.2545 2.24863 12.7126 2.24863C12.1707 2.24863 11.6342 2.35539 11.1336 2.56281C10.633 2.77023 10.1782 3.07425 9.79509 3.4575L9.00009 4.2525L8.20509 3.4575C7.43132 2.68373 6.38186 2.24903 5.28759 2.24903C4.19331 2.24903 3.14386 2.68373 2.37009 3.4575C1.59632 4.23127 1.16162 5.28072 1.16162 6.375C1.16162 7.46927 1.59632 8.51873 2.37009 9.2925L3.16509 10.0875L9.00009 15.9225L14.8351 10.0875L15.6301 9.2925C16.0133 8.90943 16.3174 8.45461 16.5248 7.95401C16.7322 7.45342 16.839 6.91686 16.839 6.375C16.839 5.83313 16.7322 5.29657 16.5248 4.79598C16.3174 4.29539 16.0133 3.84056 15.6301 3.4575Z" stroke="#535353" stroke-linecap="round" stroke-linejoin="round"/></svg>'
298                + '<svg class="dark_heart" width="18" height="17" viewBox="0 0 18 17" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M15.6301 3.4575C15.247 3.07425 14.7922 2.77023 14.2916 2.56281C13.791 2.35539 13.2545 2.24863 12.7126 2.24863C12.1707 2.24863 11.6342 2.35539 11.1336 2.56281C10.633 2.77023 10.1782 3.07425 9.79509 3.4575L9.00009 4.2525L8.20509 3.4575C7.43132 2.68373 6.38186 2.24903 5.28759 2.24903C4.19331 2.24903 3.14386 2.68373 2.37009 3.4575C1.59632 4.23127 1.16162 5.28072 1.16162 6.375C1.16162 7.46927 1.59632 8.51873 2.37009 9.2925L3.16509 10.0875L9.00009 15.9225L14.8351 10.0875L15.6301 9.2925C16.0133 8.90943 16.3174 8.45461 16.5248 7.95401C16.7322 7.45342 16.839 6.91686 16.839 6.375C16.839 5.83313 16.7322 5.29657 16.5248 4.79598C16.3174 4.29539 16.0133 3.84056 15.6301 3.4575Z" fill="#535353" stroke="#535353" stroke-linecap="round" stroke-linejoin="round"/></svg>'
299                + '</button>'
300            : '';
301
302        /* Нижняя строка: цвета + кнопка избранного */
303        var footer = (colorsHtml || favBtn)
304            ? '<div class="app-desktop-search__product-footer">'
305                + colorsHtml
306                + favBtn
307                + '</div>'
308            : '';
309
310        /* Перелистывание фото — прозрачные зоны поверх картинки */
311        var photosRow = '';
312        var photos = item.photos && item.photos.length ? item.photos : [];
313        if (photos.length > 1) {
314            photosRow = '<div class="app-desktop-search__photos-row">'
315                + photos.map(function (url) {
316                    return '<div class="app-desktop-search__photos-zone" data-photo="' + escapeHtml(url) + '"></div>';
317                }).join('')
318                + '</div>';
319        }
320
321        /* Размерная сетка — появляется при наведении поверх нижней части фото */
322        var sizesBlock = '';
323        var offers = item.offers && item.offers.length ? item.offers : [];
324        var allSizesOnlyInStore = offers.length > 0 && offers.every(offerIsOnlyInStore);
325        var storeOnlyBadge = allSizesOnlyInStore
326            ? '<div class="app-desktop-search__product-store-only"><span class="app-desktop-search__product-store-only__text">Только в магазинах</span></div>'
327            : '';
328
329        if (offers.length) {
330            var sizeItems = offers.map(function (offer) {
331                var onlyInStore = offerIsOnlyInStore(offer);
332                var onlyInStoreClass = onlyInStore ? ' only_in_store' : '';
333                return '<div class="app-desktop-search__size-item' + onlyInStoreClass + '"'
334                    + ' data-id="' + (offer.id || '') + '"'
335                    + ' onclick="addToBasketAll(' + (offer.id || '') + ', null, \'Поиск\')">'
336                    + '<div class="size-item__size">' + escapeHtml(offer.size || '') + '</div>'
337                    + '</div>';
338            }).join('');
339
340            var hasMore = offers.length > 4;
341            sizesBlock = '<div class="app-desktop-search__product-sizes main-product-hover-block">'
342                + '<div class="add-to-basket">
342Добавить в корзину</div>'
343                + '<div class="sizes-block">'
344                +   '<div class="sizes-line" data-count="4">' + sizeItems + '</div>'
345                +   '<div class="left-arrow disabled">'
346                +     '<svg class="left-arrow_disabled" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.5 5.5L7.75 10.25L12.5 15" stroke="#DEDEDE" stroke-linecap="round" stroke-linejoin="round"/></svg>'
347                +     '<svg class="left-arrow_active" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.5 5.5L7.75 10.25L12.5 15" stroke="#212121" stroke-linecap="round" stroke-linejoin="round"/></svg>'
348                +   '</div>'
349                +   '<div class="right-arrow ' + (hasMore ? 'active' : 'disabled') + '">'
350                +     '<svg class="right-arrow_disabled" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 14.5L12.25 9.75L7.5 5" stroke="#DEDEDE" stroke-linecap="round" stroke-linejoin="round"/></svg>'
351                +     '<svg class="right-arrow_active" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 14.5L12.25 9.75L7.5 5" stroke="#535353" stroke-linecap="round" stroke-linejoin="round"/></svg>'
352                +   '</div>'
353                + '</div>'
354                + '</div>';
355        }
356
357        return '<a class="app-desktop-search__product-card" href="' + escapeHtml(item.url || '#') + '">'
358            + '<div class="app-desktop-search__product-img-wrap">'
359            +   '<img class="app-desktop-search__product-img" src="' + escapeHtml(item.image || '') + '" alt="' + escapeHtml(item.name || '') + '" loading="lazy">'
360            +   badge
361            +   storeOnlyBadge
362            +   photosRow
363            +   sizesBlock
364            + '</div>'
365            + '<div class="app-desktop-search__product-info">'
366            +   meta
367            +   '<span class="app-desktop-search__product-name">' + escapeHtml(item.name || '') + '</span>'
368            +   prices
369            +   footer
370            + '</div>'
371            + '</a>';
372    }
373
374    function buildStars(rating) {
375        var count = Math.round(parseFloat(rating) || 5);
376        var html = '';
377        for (var i = 0; i < 5; i++) {
378            html += '<svg class="app-desktop-search__product-star" viewBox="0 0 15 15" fill="' + (i < count ? '#525252' : 'none') + '" xmlns="http://www.w3.org/2000/svg">'
379                + '<path d="M7.5 1l1.545 4.756H14l-4.045 2.938 1.545 4.756L7.5 10.512l-3.999 2.938 1.545-4.756L1 5.756h4.955L7.5 1Z" stroke="#525252" stroke-width="1"/>'
380                + '</svg>';
381        }
382        return html;
383    }
384
385    /* ------------------------------------------------------------------ */
386    /* Слайдер размеров в карточках автодополнения                        */
387    /* ------------------------------------------------------------------ */
388
389    function getSizesItemsPerLine($sizesLine) {
390        // Сколько элементов реально поместится в ширину одного ряда.
391        // Приоритет: 4 в ряд. Если не влезает — уменьшаем ширину items на JS.
392        // Далее по убыванию: 3 -> 2 -> 1.
393        if (!$sizesLine || !$sizesLine.length) return 4;
394
395        var items = $sizesLine.find('.app-desktop-search__size-item').toArray();
396        if (!items.length) return 4;
397
398        var lineEl = $sizesLine[0];
399        var containerWidth = lineEl.clientWidth || 0;
400
401        // Важно: `sizes-line` внутри hover-блока (`display: none`) на этапе init
402        // может иметь clientWidth = 0. Тогда берём ширину видимого контейнера карточки.
403        if (!containerWidth) {
404            var $imgWrap = $sizesLine.closest('.app-desktop-search__product-img-wrap');
405            if ($imgWrap && $imgWrap.length) {
406                containerWidth = $imgWrap[0].clientWidth || 0;
407            }
408        }
409
410        if (!containerWidth) return 4;
411
412        var itemEl = items[0];
413        var styles = window.getComputedStyle(itemEl);
414        var marginLeft = parseFloat(styles.marginLeft) || 0;
415        var marginRight = parseFloat(styles.marginRight) || 0;
416        var marginTotal = marginLeft + marginRight;
417
418        // Доступная ширина под элементы размеров:
419        // ширина sizes-line минус фиксированный резерв под стрелки.
420        // По требованию: sizes-line - 60px.
421        var availableWidth = Math.max(0, containerWidth - 60);
422        if (!availableWidth) return 1;
423
424        function applyWidth(widthPx) {
425            // Меняем только локальные инлайн-стили у элементов в этом hover-блоке.
426            // CSS-модель: width/min-width + flex-basis.
427            for (var i = 0; i < items.length; i++) {
428                items[i].style.width = widthPx + 'px';
429                items[i].style.minWidth = widthPx + 'px';
430                items[i].style.flex = '0 0 ' + widthPx + 'px';
431            }
432        }
433
434        // Если длина значения размера > 6 символов — оно "точно не влезет в 2 строки".
435        // Значит, заранее уменьшаем кол-во размеров в ряд (даём ячейкам больше ширины).
436        var maxSizeLen = 0;
437        for (var i = 0; i < items.length; i++) {
438            var sizeTextEl = items[i].querySelector('.size-item__size');
439            var text = sizeTextEl ? (sizeTextEl.textContent || '') : (items[i].textContent || '');
440            text = String(text).trim();
441            if (text.length > maxSizeLen) maxSizeLen = text.length;
442        }
443
444        var forceSmallerPerLine = maxSizeLen > 6;
445
446        // Минимальная ширина одной ячейки (защита от слишком узкого текста).
447        // Для длинных значений делаем её выше.
448        var MIN_ITEM_WIDTH = forceSmallerPerLine ? 40 : 18;
449
450        // Подбираем perLine и ширину:
451        // widthPerItem = availableWidth / perLine - (marginLeft + marginRight)
452        var candidates = forceSmallerPerLine ? [2, 1] : [4, 3, 2, 1];
453        for (var c = 0; c < candidates.length; c++) {
454            var perLine = candidates[c];
455            var widthPerItem = Math.floor(availableWidth / perLine - marginTotal);
456            if (widthPerItem >= MIN_ITEM_WIDTH) {
457                applyWidth(widthPerItem);
458                return perLine;
459            }
460        }
461
462        // Если даже при самом маленьком перLine не хватает, оставим текущее (fallback 1).
463        return 1;
464    }
465
466    function initSizesSlider(container) {
467        var lines = container ? container.querySelectorAll('.main-product-hover-block .sizes-line') : [];
468        Array.prototype.forEach.call(lines, function (line) {
469            var $line = $(line);
470            var items = Array.prototype.slice.call(line.querySelectorAll('.app-desktop-search__size-item'));
471            var perLine = getSizesItemsPerLine($line);
472            if (items.length <= perLine) return;
473            for (var i = perLine; i < items.length; i++) {
474                items[i].style.display = 'none';
475            }
476        });
477    }
478
479    /**
480     * При hover на карточке поднимает плашку «Только в магазинах»:
481     * bottom = высота нижнего оверлея (.main-product-hover-block: «Добавить в корзину» + сетка размеров) + 10px.
482     */
483    function applyStoreOnlyBadgeBottomForCardHover($card) {
484        var $badge = $card.find('.app-desktop-search__product-store-only');
485        if (!$badge.length) {
486            return;
487        }
488        var $hover = $card.find('.main-product-hover-block');
489        if (!$hover.length) {
490            return;
491        }
492        var h = $hover.outerHeight(true) || $hover.outerHeight();
493        if (!h && $hover[0]) {
494            h = $hover[0].getBoundingClientRect().height;
495        }
496        $badge.css('bottom', Math.round(h) + 10 + 'px');
497    }
498
499    function resetStoreOnlyBadgeBottomForCard($card) {
500        $card.find('.app-desktop-search__product-store-only').css('bottom', '');
501    }
502
503    $(document).on('mouseenter', '.js-search-products-grid .app-desktop-search__product-card', function () {
504        var $card = $(this);
505        if (!$card.find('.app-desktop-search__product-store-only').length) {
506            return;
507        }
508        var el = this;
509        window.requestAnimationFrame(function () {
510            window.requestAnimationFrame(function () {
511                applyStoreOnlyBadgeBottomForCardHover($(el));
512            });
513        });
514    });
515
516    $(document).on('mouseleave', '.js-search-products-grid .app-desktop-search__product-card', function () {
517        resetStoreOnlyBadgeBottomForCard($(this));
518    });
519
520    /* Клик по блоку размеров и кнопке избранного не должен переходить по ссылке карточки */
521    $(document).on('click', '.js-search-products-grid .main-product-hover-block', function (e) {
522        e.preventDefault();
523        e.stopPropagation();
524    });
525
526    $(document).on('click', '.js-search-products-grid .app-desktop-search__fav-btn', function (e) {
527        e.preventDefault();
528        e.stopPropagation();
529    });
530
531    /* Стрелка вправо — сдвигаем окно видимых размеров */
532    $(document).on('click', '.js-search-products-grid .right-arrow.active', function (e) {
533        e.preventDefault();
534        e.stopPropagation();
535        var $sizesLine = $(this).siblings('.sizes-line');
536        var items = $sizesLine.find('.app-desktop-search__size-item').toArray();
537        var perLine = getSizesItemsPerLine($sizesLine);
538        var firstVisible = -1;
539        for (var i = 0; i < items.length; i++) {
540            if ($(items[i]).css('display') !== 'none') { firstVisible = i; break; }
541        }
542        if (firstVisible < 0 || firstVisible + perLine >= items.length) return;
543        $(items[firstVisible]).hide();
544        $(items[firstVisible + perLine]).show();
545
546        var newFirst = firstVisible + 1;
547        $(this).siblings('.left-arrow').toggleClass('disabled', newFirst === 0).toggleClass('active', newFirst > 0);
548        var lastVisible = newFirst + perLine - 1;
549        $(this).toggleClass('active', lastVisible < items.length - 1).toggleClass('disabled', lastVisible >= items.length - 1);
550    });
551
552    /* Стрелка влево — сдвигаем окно видимых размеров */
553    $(document).on('click', '.js-search-products-grid .left-arrow.active', function (e) {
554        e.preventDefault();
555        e.stopPropagation();
556        var $sizesLine = $(this).siblings('.sizes-line');
557        var items = $sizesLine.find('.app-desktop-search__size-item').toArray();
558        var perLine = getSizesItemsPerLine($sizesLine);
559        var firstVisible = -1;
560        for (var i = 0; i < items.length; i++) {
561            if ($(items[i]).css('display') !== 'none') { firstVisible = i; break; }
562        }
563        if (firstVisible <= 0) return;
564        $(items[firstVisible - 1]).show();
565        $(items[firstVisible + perLine - 1]).hide();
566
567        var newFirst = firstVisible - 1;
568        $(this).toggleClass('disabled', newFirst === 0).toggleClass('active', newFirst > 0);
569        var lastVisible = newFirst + perLine - 1;
570        $(this).siblings('.right-arrow').toggleClass('active', lastVisible < items.length - 1).toggleClass('disabled', lastVisible >= items.length - 1);
571    });
572
573    /* Смена фото при наведении на зону photos-row */
574    $(document).on('mouseenter', '.app-desktop-search__photos-zone', function () {
575        var url = $(this).data('photo');
576        if (url) {
577            $(this).closest('.app-desktop-search__product-img-wrap')
578                .find('.app-desktop-search__product-img')
579                .attr('src', url);
580        }
581    });
582
583    var PRODUCTS_VISIBLE = 4;
584
585    function renderProducts(products, query) {
586        var $productsBlock = $('.js-search-products');
587        var $grid          = $('.js-search-products-grid');
588        var $footer        = $('.js-search-products-footer');
589        var $viewAll = $('.js-search-view-all');
590
591        if (!products || !products.length) {
592            $grid.html('');
593            $footer.hide();
594            $productsBlock.hide();
595            return;
596        }
597
598        /* Показываем не более 4 карточек */
599        $grid.html(products.slice(0, PRODUCTS_VISIBLE).map(buildProductCard).join(''));
600        $productsBlock.show();
601
602        /* Инициализируем слайдер размеров в только что отрендеренных карточках */
603        initSizesSlider($grid[0]);
604
605        /* Кнопка «Смотреть все результаты» — только если товаров больше 4 */
606        if (products.length > PRODUCTS_VISIBLE) {
607            var searchUrl = '/search/?q=' + encodeURIComponent(query || '');
608            $viewAll.attr('href', searchUrl);
609            $footer.show();
610        } else {
611            $footer.hide();
612        }
613    }
614
615    function updateAutocompleteVisibility() {
616        var $acBody = $('.js-search-ac-body');
617        var hasTaps = $('.app-desktop-search__taps .app-desktop-search__tap').length > 0;
618        var hasHistory = $('.js-search-history-list .app-desktop-search__popup-link').length > 0;
619        var hasPopular = $('.js-search-popular-list .app-desktop-search__popup-link').length > 0;
620        var hasCategories = $('.js-search-categories-list .app-desktop-search__popup-link').length > 0;
621        var hasProducts = $('.js-search-products-grid .app-desktop-search__product-card').length > 0;
622
623        var hasAnyContent = hasTaps || hasHistory || hasPopular || hasCategories || hasProducts;
624        if (hasAnyContent) {
625            $acBody.addClass('is-open');
626        } else {
627            $acBody.removeClass('is-open');
628        }
629    }
630
631    /* ------------------------------------------------------------------ */
632    /* AJAX: запрос подсказок                                              */
633    /* ------------------------------------------------------------------ */
634
635    function getSuggestions() {
636        var query = $('#header-search-input').val().trim();
637        $('.app-desktop-search-btn').removeClass('for-close');
638
639        function requestAutocompleteRich(requestQuery) {
640            return BX.ajax.runComponentAction(
641                'markformelle:mf.search.autocomplete',
642                'getAutocompleteRich',
643                {
644                    mode: 'class',
645                    data: { q: requestQuery },
646                }
647            );
648        }
649
650        function renderAutocompleteData(data) {
651            lastCorrection = (data.correction || '').trim();
652            renderTaps(data.taps || []);
653            renderSuggestions(data.phrases || []);
654            renderCategories(data.categories || []);
655            renderProducts(data.products || [], query);
656            updateAutocompleteVisibility();
657            clearActiveItem();
658        }
659
660        /* Всегда идём на сервер — при пустом запросе Anyquery вернёт дефолтные данные */
661        requestAutocompleteRich(query).then(function (response) {
662            var data = response.data || {};
663
664            // Для непустого запроса при пустых товарах повторяем запрос без текста.
665            if (query !== '' && (!data.products || !data.products.length)) {
666                requestAutocompleteRich('').then(function (fallbackResponse) {
667                    renderAutocompleteData(fallbackResponse.data || {});
668                }, function () {
669                    renderAutocompleteData(data);
670                });
671                return;
672            }
673
674            renderAutocompleteData(data);
675        }, function () {
676            lastCorrection = '';
677            renderTaps([]);
678            renderSuggestions([]);
679            renderCategories([]);
680            renderProducts([], query);
681            updateAutocompleteVisibility();
682        });
683    }
684
685    /* ------------------------------------------------------------------ */
686    /* Вспомогательные                                                     */
687    /* ------------------------------------------------------------------ */
688
689    function declOfNum(n, titles) {
690        var cases = [2, 0, 1, 1, 1, 2];
691        return titles[(n % 100 > 4 && n % 100 < 20) ? 2 : cases[Math.min(n % 10, 5)]];
692    }
693
694    function escapeHtml(str) {
695        return String(str)
696            .replace(/&/g, '&amp;')
697            .replace(/</g, '&lt;')
698            .replace(/>/g, '&gt;')
699            .replace(/"/g, '&quot;');
700    }
701
702    function debounce(func, wait) {
703        var timeout;
704        return function () {
705            var ctx  = this;
706            var args = arguments;
707            clearTimeout(timeout);
708            timeout = setTimeout(function () { func.apply(ctx, args); }, wait);
709        };
710    }
711
712    var waitForDigi = (function () {
713        var promise = null;
714
715        return function () {
716            if (typeof window === 'undefined') {
717                return Promise.resolve(false);
718            }
719
720            if (promise) return promise;
721
722            promise = new Promise(function (resolve) {
723                function checkDigi() {
724                    if (window.Digi && window.Digi.api) {
725                        resolve(true);
726                        return;
727                    }
728                    setTimeout(checkDigi, 100);
729                }
730
731                checkDigi();
732            });
733
734            return promise;
735        };
736    })();
737
738    function trackAutocompleteQueryClick(itemText) {
739        if (!itemText || typeof window === 'undefined') return;
740
741        var searchTerm = ($('#header-search-input').val() || '').trim();
742        var correction = (lastCorrection || '').trim();
743
744        waitForDigi().then(function (isReady) {
745            if (!isReady) return;
746            if (!window.Digi || !window.Digi.api || typeof window.Digi.api.sendAutocompleteQueryClickEvent !== 'function') return;
747
748            // Формат параметров строго по Digi.API:
749            // { searchTerm, correction, item }
750            window.Digi.api.sendAutocompleteQueryClickEvent({
751                searchTerm: searchTerm,
752                correction: correction,
753                item: itemText
754            });
755        });
756    }
757
758    function updateClearBtn() {
759        var $input = $('#header-search-input');
760        $input.closest('.app-desktop-search__input-wrap')
761            .toggleClass('has-value', $input.val().length > 0);
762    }
763
764    /* ------------------------------------------------------------------ */
765    /* Обработчики событий                                                 */
766    /* ------------------------------------------------------------------ */
767
768    $(document).on('input', '#header-search-input', debounce(getSuggestions, 350));
769    $(document).on('input', '#header-search-input', updateClearBtn);
770
771    /* Фокус на инпуте — запрашиваем дефолтные данные с сервера */
772    $(document).on('focus', '#header-search-input', function () {
773        if (!$('.js-search-ac-body').hasClass('is-open')) {
774            getSuggestions();
775        }
776    });
777
778    /* Очистить инпут — показываем дефолтный стейт */
779    $(document).on('click', '.js-search-clear', function (e) {
780        e.preventDefault();
781        $('#header-search-input').val('');
782        updateClearBtn();
783        getSuggestions();
784        $('#header-search-input').trigger('focus');
785    });
786
787    /* Закрыть попап */
788    $(document).on('click', '.js-search-close, .app-desktop-search__close-btn', function (e) {
789        e.preventDefault();
790        e.stopPropagation();
791        closeSearchPopup();
792    });
793
794    /* Десктоп: клик по search-lupa-btn должен закрывать автодополнение
795     * (внешний контроллер закрывает попап/оверлей, но closeSearchPopup может не вызываться).
796     */
797    $(document).on('click', '.search-lupa-btn', function () {
798        var $acBody = $('.js-search-ac-body');
799        if ($acBody.hasClass('is-open')) {
800            closeSearchPopup();
801        }
802    });
803
804    /* Очистить историю */
805    $(document).on('click', '.js-search-history-clear', function (e) {
806        e.preventDefault();
807        e.stopPropagation();
808
809        clearHistory();
810
811        // Не закрываем попап: обновляем дефолтные подсказки заново,
812        // чтобы блок "Часто ищут" / категории остались видимыми.
813        getSuggestions();
814    });
815
816    /* Сохранение в историю при клике на подсказку */
817    $(document).on('click', '.js-search-history-list .app-desktop-search__popup-link, .js-search-popular-list .app-desktop-search__popup-link', function () {
818        var queryText = $(this).find('span').text().trim();
819        saveToHistory(queryText);
820    });
821
822    /* Digi.API: клик по подсказке в блоке "Часто ищут" */
823    $(document).on('click', '.js-search-popular-list .app-desktop-search__popup-link', function () {
824        var queryText = $(this).find('span').text().trim();
825        trackAutocompleteQueryClick(queryText);
826    });
827
828    /* Сохранение в историю при сабмите формы */
829    $(document).on('submit', '.app-desktop-search__form form', function () {
830        saveToHistory($('#header-search-input').val().trim());
831    });
832
833    /* Клавиатурная навигация */
834    $(document).on('keydown', '#header-search-input', function (e) {
835        var $items = $('.app-desktop-search__popup-link');
836
837        if (e.key === 'Escape') {
838            closeSearchPopup();
839            return;
840        }
841
842        if (!$items.length) return;
843
844        if (e.key === 'ArrowDown') {
845            e.preventDefault();
846            setActiveItemByIndex(getActiveItemIndex() + 1);
847        } else if (e.key === 'ArrowUp') {
848            e.preventDefault();
849            setActiveItemByIndex(getActiveItemIndex() - 1);
850        } else if (e.key === 'Enter') {
851            var activeIndex = getActiveItemIndex();
852            if (activeIndex >= 0) {
853                e.preventDefault();
854                $items.eq(activeIndex).get(0).click();
855            }
856        }
857    });
858
859    $(document).on('mouseenter', '.app-desktop-search__popup-link', function () {
860        clearActiveItem();
861        $(this).addClass(ACTIVE_ITEM_CLASS);
862    });
863
864    /* Клик вне блока автодополнения
865     * Важно: на странице поиска одновременно может быть рендер двух шаблонов autocomplete
866     * (`.default` и `header_any`). Поэтому закрываем только popup header_any и только
867     * когда он реально открыт, а также игнорируем клики внутри поискового блока `.default`.
868     */
869    $('body').on('click.mfSearchAutocompleteHeaderAny', function (e) {
870        // Если попап header_any не открыт — ничего не делаем, чтобы не влиять на `.default`.
871        if (!$('.js-search-ac-body').hasClass('is-open')) {
872            return;
873        }
874
875        var $t = $(e.target);
876        if (
877            !$t.closest('.js-search-ac-body').length &&
878            !$t.closest('.js-search-popup').length &&
879            !$t.closest('.search-lupa-btn').length
880        ) {
881            closeSearchPopup();
882        }
883    });
884});

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.