PageSourceSearch

https://wau.ru/local/templates/wau_common/js/common.js?178938886037345

js wau.ru collected 2026-10-01 15:15:10 UTC 37,345 bytes, 881 lines download raw bytes

1$(function ()  {
2    initFiltersShowToggle();
3    initPixelGoal();
4    //Функция initReadMoreToggle() для отображения короткого вида текста с кнопками показать/скрыть. Используется на главной странице в блоке Наша история и на странице карточки товара в описании
5    initReadMoreToggle();
6    //Функция для подгрузки начисляемых бонусов из црм
7    BonusElementListSite();
8
9    initBonusTooltips();
10    //инициализация масок
11    initAllPhoneInputs();
12    // После инициализации всех полей синхронизируем со стартовым выбором страны
13    syncPhoneInputsFromCountrySelect();
14
15    
16    // Закрытие тултипов при клике вне и scroll
17    document.addEventListener('click', hideAllTooltips);
18    window.addEventListener('scroll', hideAllTooltips);
19    window.addEventListener('touchstart', hideAllTooltips);
20    window.addEventListener('touchmove', hideAllTooltips);
21
22
23    initVideoReviewModalClose();
24    initCartLinkFallback();
25    initHeroBannerLoading();
26    initLazyBackgroundFallback();
27    initModalTapProbe();
28})
29
30/*
31 * Сторож нажатий по окнам.
32 *
33 * Владелец 6 сентября 2026: на айфоне не открывается выбор пункта выдачи и не
34 * закрывается окно входа, а через время оживают оба сразу. Воспроизвести не
35 * удалось — в браузере разработчика всё исправно, обработчики на месте.
36 *
37 * Поэтому спрашиваем сам телефон: записываем, был ли обработчик готов в момент
38 * касания и на какой секунде страница ожила. Личного в записи ничего нет.
39 *
40 * Временная мера на время разбирательства — снять, как только причина найдена.
41 */
42/*
43 * Открываем и закрываем окна сами.
44 *
45 * Журнал сторожа с телефона владельца, ночь на 6 сентября 2026, показал то,
46 * чего мы не ожидали: **каждое** нажатие приходилось по готовому окну —
47 * и на девятнадцатой секунде, и на сто третьей, — а страница оживала за
48 * 0,7–7 секунд. То есть Foundation поднят, окна инициализированы, касание до
49 * страницы доходит. И всё равно ничего не открывалось.
50 *
51 * Значит между касанием и обработчиком Foundation, навешенным на документ,
52 * событие кто-то перехватывает. Кто именно — вопрос открытый, но и не важный:
53 * раз окно готово, его можно открыть напрямую, ничего ни у кого не спрашивая.
54 *
55 * Двойного срабатывания это не создаёт: замерено на боевом сайте — на одно
56 * нажатие приходит одно событие открытия, лишних запросов за корзиной нет.
57 * Открытие уже открытого окна и закрытие закрытого Foundation пропускает.
58 */
59function открытьОкно(имя) {
60    try {
61        var окно = document.getElementById(имя);
62
63        if (окно && getComputedStyle(окно).display === 'none') {
64            jQuery(окно).foundation('open');
65        }
66    } catch (e) {
67        // Не вышло — останется прежний путь через Foundation.
68    }
69}
70
71function закрытьОкно(имя) {
72    try {
73        var окно = document.getElementById(имя);
74
75        if (окно && getComputedStyle(окно).display !== 'none') {
76            jQuery(окно).foundation('close');
77        }
78    } catch (e) {
79        // Не вышло — останется прежний путь через Foundation.
80    }
81}
82
83/*
84 * Баннеры главной приезжают по мере надобности.
85 *
86 * Виден один, а грузились все девять — около 550 КБ, из которых человек видит
87 * восьмую часть. И, что важнее, первый баннер делил канал с восемью соседями,
88 * хотя именно он и есть самый крупный элемент первого экрана: по Ð
88½ÐµÐ¼Ñƒ считается
89 * LCP.
90 *
91 * Адреса всех баннеров, кроме первого, лежат в `data-` — браузер не может
92 * загрузить то, чего в `srcset` нет. Подставляем их сами.
93 *
94 * Почему не `loading="lazy"`: пробовали, и это стоило пустого баннера на живом
95 * сайте 6 сентября 2026. Slick двигает слайды через `transform`, а по такому
96 * перемещению браузер загрузку не начинает — слайд становится активным, а
97 * картинки у него нет.
98 *
99 * Поэтому здесь два независимых пути, и второй страхует первый:
100 *
101 *  — показываем соседей текущего слайда при каждой смене;
102 *  — через три секунды показываем всё оставшееся, что бы ни случилось с
103 *    событиями slick.
104 *
105 * Три секунды выбраны не наугад: автопрокрутка меняет слайд каждые четыре, то
106 * есть к появлению второго баннера его картинка уже в пути. Пустого слайда не
107 * может быть даже если события карусели откажут совсем.
108 */
109function initHeroBannerLoading() {
110    var слайдер = document.querySelector('.hero__slider');
111
112    if (!слайдер) {
113        return;
114    }
115
116    var показать = function (слайд) {
117        if (!слайд || !слайд.querySelectorAll) {
118            return;
119        }
120
121        var узлы = слайд.querySelectorAll('source[data-srcset], img[data-src]');
122
123        Array.prototype.forEach.call(узлы, function (узел) {
124            var srcset = узел.getAttribute('data-srcset');
125            var src = узел.getAttribute('data-src');
126
127            if (srcset) {
128                узел.setAttribute('srcset', srcset);
129                узел.removeAttribute('data-srcset');
130            }
131
132            if (src) {
133                узел.setAttribute('src', src);
134                узел.removeAttribute('data-src');
135            }
136        });
137    };
138
139    var показатьВокруг = function () {
140        var активный = слайдер.querySelector('.slick-current');
141
142        if (!активный) {
143            показатьВсе();
144
145            return;
146        }
147
148        [активный, активный.nextElementSibling, активный.previousElementSibling].forEach(показать);
149    };
150
151    var показатьВсе = function () {
152        Array.prototype.forEach.call(слайдер.querySelectorAll('.hero__slide'), показать);
153    };
154
155    var показатьТекущий = function () {
156        var текущий = слайдер.querySelector('.slick-current');
157
158        if (!текущий) {
159            показатьВсе();
160
161            return;
162        }
163
164        показать(текущий);
165    };
166
167    /*
168     * Сначала — только тот слайд, который человек видит. Соседей грузим, когда
169     * страница освободится: автопрокрутка меняет слайд раз в четыре секунды, и
170     * до этого момента соседи не нужны никому.
171     *
172     * Замерено на боевой главной 6 сентября 2026: соседние баннеры приезжали
173     * на 0,82 секунде, и вместе с текущим это 326 КБ — они делили канал с
174     * первым баннером, по которому считается LCP.
175     *
176     * Ручное листание при этом не страдает: `beforeChange` подписан ниже и
177     * подтягивает соседей до того, как слайд поедет.
178     */
179    показатьТекущий();
180
181    var показатьСоседей = function () {
182        setTimeout(показатьВокруг, 300);
183    };
184
185    if (document.readyState === 'complete') {
186        показатьСоседей();
187    } else {
188        window.addEventListener('load', показатьСоседей);
189    }
190
191    try {
192        jQuery(слайдер).on('init reInit afterChange beforeChange', показатьВокруг);
193    } catch (e) {
194        // Событий не будет — останется страховка ниже.
195    }
196
197    /*
198     * Страховка на случай, если события карусели не придут: раскрываем слайды
199     * сами — но по одному, в такт автопрокрутке, а не все разом.
200     *
201     * Раньше здесь стояло `setTimeout(показатьВсе, 3000)`, и на четвёртой
202     * секунде браузер забирал шесть баннеров одним куском — 223 КБ. Замер
203     * главной 8 сентября 2026: 4207 мс, шесть запросов подряд. Между тем
204     * карусель меняет слайд раз в четыре секунды, то есть шестой баннер нужен
205     * человеку на двадцатой секунде — если он вообще досмотрит.
206     *
207     * Раскрывать по одному безопасно: `показатьВокруг` на событиях карусели
208     * идёт впереди этого таймера и успевает подготовить соседей, так что
209     * обычно раскрывать уже нечего. А если событий нет вовсе, таймер сам
210     * доберётся до очередного слайда раньше, чем тот понадобится, — ради
211     * этого страховка и заводилась 6 сентября, когда человек увидел пустое
212     * белое поле во весь экран.
213     */
214    var ТАКТ_КАРУСЕЛИ_МС = 4000;
215
216    var раскрытьОчередной = function () {
217        var слайды = слайдер.querySelectorAll('.hero__slide');
218
219        for (var i = 0; i < слайды.length; i++) {
220            if (!слайды[i].querySelector('source[data-srcset], img[data-src]')) {
221                continue;
222            }
223
224            показать(слайды[i]);
225            setTimeout(раскрытьОчередной, ТАКТ_КАРУСЕЛИ_МС);
226
227            return;
228        }
229    };
230
231    setTimeout(раскрытьОчередной, 3000);
232}
233
234/*
235 * Страховка на случай, если LazyLoad не поднялся.
236 *
237 * Фоны плиток категорий переведены на отложенную загрузку: адрес лежит в
238 * `data-bg`, а подставляет его LazyLoad. Библиотека может не приехать —
239 * блокировщик, обрыв сети, ошибка в соседнем скрипте, — и тогда человек
240 * увидит плитки без картинок. Пустой блок хуже лишних килобайт, поэтому
241 * через две секунды проверяем, есть ли библиотека, и если её нет —
242 * проставляем фоны сами.
243 *
244 * Проверяем именно наличие библиотеки, а не факт загрузки картинок: если
245 * LazyLoad на месте, он сам решит, когда грузить, и вмешиваться незачем.
246 */
247function initLazyBackgroundFallback() {
248    var подставить = function () {
249        var узлы = document.querySelectorAll('[data-bg]');
250
251        Array.prototype.forEach.call(узлы, function (узел) {
252            var адрес = узел.getAttribute('data-bg');
253
254            if (!адрес) {
255                return;
256            }
257
258            узел.style.backgroundImage = 'url("' + адрес + '")';
259            узел.removeAttribute('data-bg');
260        });
261    };
262
263    setTimeout(function () {
264        if (typeof LazyLoad === 'undefined') {
265            подставить();
266        }
267    }, 2000);
268}
269
270function initModalTapProbe() {
271    var сообщить = function (что, мс) {
272        try {
273            if (!navigator.sendBeacon) {
274                return;
275            }
276
277            navigator.sendBeacon(
278                '/local/ajax/payment_probe.php',
279                new Blob(
280                    [JSON.stringify({ order: 1, state: что, ms: Math.round(мс || 0) })],
281                    { type: 'application/json' }
282                )
283            );
284        } catch (e) {
285            // Сторож наблюдает, а не мешает.
286        }
287    };
288
289    document.addEventListener('click', function (event) {
290        if (!event.target.closest) {
291            return;
292        }
293
294        var сейчас = window.performance && performance.now ? performance.now() : 0;
295
296        var крестик = event.target.closest('[data-close]');
297
298        if (крестик) {
299            var окно = крестик.closest('.reveal');
300            var готово = !!(окно && окно.id && isModalReady(окно.id));
301
302            сообщить(готово ? 'tap-close-ready' : 'tap-close-not-ready', сейчас);
303
304            if (готово) {
305                закрытьОкно(окно.id);
306            }
307
308            return;
309        }
310
311        var триггер = event.target.closest('[data-open]');
312
313        if (триггер) {
314            var имя = триггер.getAttribute('data-open');
315            var можно = !!(имя && isModalReady(имя));
316
317            сообщить(можно ? 'tap-open-ready' : 'tap-open-not-ready', сейчас);
318
319            if (можно) {
320                открытьОкно(имя);
321            }
322        }
323    }, true);
324
325    // И отдельно — когда страница вообще ожила.
326    var часы = setInterval(function () {
327        var любое = document.querySelector('.reveal[id]');
328
329        if (!любое || !isModalReady(любое.id)) {
330            return;
331        }
332
333        clearInterval(часы);
334        сообщить('ui-alive', window.performance && performance.now ? performance.now() : 0);
335    }, 100);
336
337    setTimeout(function () {
338        clearInterval(часы);
339    }, 60000);
340}
341
342function initFiltersShowToggle() {
343    const limitHeight = 164;
344
345    const $block = $('.filter__swtchrs_big');
346    const $btn = $('.filter__show');
347    const $cloud = $('.filter__cloud');
348
349    if (!$block.length || !$btn.length) return;
350
351    const checkHeight = () => {
352        const hasLimit = $block.hasClass('filter__swtchrs_height');
353
354        if (hasLimit) {
355            $block.removeClass('filter__swtchrs_height');
356        }
357
358        const realHeight = $block.outerHeight();
359
360        if (hasLimit) {
361            $block.addClass('filter__swtchrs_height');
362        }
363
364        const shouldHide = realHeight <= limitHeight;
365
366        $btn.toggleClass('hide', shouldHide);
367        $cloud.toggleClass('hide', shouldHide);
368
369        if (!shouldHide) {
370            const imgShow = $btn.data('defaultIcon') || $btn.find('img').detach();
371
372            $block.addClass('filter__swtchrs_height');
373            $btn
374                .removeClass('filter__show_open')
375                .text('Показать все')
376                .append(imgShow);
377        }
378    };
379
380    $(document).on('click', '.filter__show', function (e) {
381        e.preventDefault();
382
383        const isCollapsed = $block.hasClass('filter__swtchrs_height');
384
385        if (isCollapsed) {
386            $block.removeClass('filter__swtchrs_height');
387            $cloud.addClass('hide');
388
389            const originalImg = $btn.find('img').detach();
390            if (!$btn.data('defaultIcon')) {
391                $btn.data('defaultIcon', originalImg);
392            }
393
394            $btn
395                .addClass('filter__show_open')
396                .text('Скрыть');
397
398        } else {
399            $block.addClass('filter__swtchrs_height');
400            $cloud.removeClass('hide');
401
402            const imgShow = $btn.data('defaultIcon') || $btn.find('img').detach();
403
404            $btn
405                .removeClass('filter__show_open')
406                .text('Показать все')
407                .append(imgShow);
408        }
409    });
410
411    checkHeight();
412
413    window.addEventListener('resize', checkHeight, true);
414}
415
416function initReadMoreToggle() {
417    const $wrappers = $('.js-card-text-wrapper');
418    if (!$wrappers.length) return;
419
420    function checkReadMore() {
421        $wrappers.each(function () {
422            const $wrapper = $(this);
423            const $text = $wrapper.find('.js-card-text');
424            const $btn = $wrapper.find('.js-card-text-toggle');
425
426            const lineHeight = parseFloat($text.css('line-height'));
427            const maxHeight = lineHeight * 12;
428
429            // Проверяем, превышает ли текст 12 лиÐ
429½Ð¸Ð¹
430            if ($text[0].scrollHeight > maxHeight) {
431                $text.addClass('read-more__text-cut');
432                $btn.addClass('visible');
433            } else {
434                $text.removeClass('read-more__text-cut');
435                $btn.removeClass('visible');
436                $wrapper.removeClass('open');
437            }
438        });
439    }
440
441    // Инициализация клика по кнопке
442    $('.js-card-text-toggle').off('click').on('click', function () {
443        const $btn = $(this);
444        const $wrapper = $btn.closest('.js-card-text-wrapper');
445        $wrapper.toggleClass('open');
446    });
447
448    // Первичная проверка
449    checkReadMore();
450
451    // Перерасчёт при ресайзе окна
452    window.addEventListener('resize', function () {
453        checkReadMore();
454    }, { passive: true });
455}
456
457
458
459function BonusElementListSite(){
460    const catalogElements = document.querySelectorAll('.product-card__info');
461    let productsData = [];
462    catalogElements.forEach((element, index) => {
463       if(element.querySelector('.product-card__prices').getAttribute('display-bonus') != 'Y'){
464           let price = element.querySelector('.product-card__price')?.textContent;
465           price = price.replace(/\D/g, '');
466           productsData.push({
467               PRODUCT_ID: parseInt(element.getAttribute('data-id')),
468               PRICE: price,
469           })
470       }
471    });
472   if(productsData.length != 0){
473       productsData = [...new Map(productsData.map(item => [item.PRODUCT_ID, item])).values()];
474       $.ajax({
475           url: "/local/ajax/loyality_product_bonus.php",
476           type: "POST",
477           dataType: 'json',
478           cache: false,
479           data: {
480               productsData: JSON.stringify(productsData),
481               answerBonus: 'Y'
482           },
483           success: function (result) {
484               if (result && typeof result === 'object' && result.success == true) {
485                   const declOfNum = (count, titles) => {
486                       const cases = [2, 0, 1, 1, 1, 2];
487                       const mod100 = count % 100;
488                       if (mod100 > 4 && mod100 < 20) {
489                           return titles[2];
490                       }
491                       const mod10 = count % 10;
492                       return titles[(mod10 < 5) ? cases[mod10] : cases[5]];
493                   };
494                   Object.keys(result.data).forEach(productId => {
495                       const bonusValue = result.data[productId];
496                       const productCard = document.querySelectorAll(`.product-card__info[data-id="${productId}"]`);
497                       if (productCard) {
498                           productCard.forEach((element, index) => {
499                             if (element.querySelector('.product-card__prices').getAttribute('display-bonus') != 'Y') {
500                                 const pricesElement = element.querySelector('.product-card__prices');
501                                 if (pricesElement ){
502                                     const bonusBadgeFull = element.querySelector('.js-bonus-badge-count');
503                                     if (bonusBadgeFull) {
504                                         const titles = ['бонус', 'бонуса', 'бонусов'];
505                                         const bonusText = '+' + bonusValue + '&nbsp;' + '<span>' + declOfNum(bonusValue, titles) + '</span>';
506                                         bonusBadgeFull.innerHTML = bonusText;
507                                     }
508                                     element.querySelector('.product-card__prices').setAttribute('display-bonus', 'Y');
509                                 }
510                               }
511                           });
512                       }
513                   });
514               }
515           },
516       });
517   }
518}
519
520//тултипы
521function createTooltip(trigger) {
522    const text = trigger.getAttribute('data-tooltip-title');
523    const tooltip = document.createElement('div');
524    tooltip.className = 'tooltip-content';
525    tooltip.textContent = text;
526    document.body.appendChild(tooltip);
527    trigger._tooltip = tooltip;
528    return tooltip;
529}
530
531function showTooltip(trigger) {
532    hideAllTooltips();
533
534    const tooltip = trigger._tooltip || createTooltip(trigger);
535    const rect = trigger.getBoundingClientRect();
536    const tooltipWidth = tooltip.offsetWidth;
537    const tooltipHeight = tooltip.offsetHeight;
538    const padding = 8;
539
540    let top = rect.top - tooltipHeight - padding;
541    let left = rect.left + rect.width / 2 - tooltipWidth / 2;
542
543    if (top < padding) {
544        top = rect.bottom + padding;
545        tooltip.classList.add('bottom');
546    } else {
547        tooltip.classList.remove('bottom');
548    }
549
550    if (left < padding) left = padding;
551    const maxLeft = window.innerWidth - tooltipWidth - padding;
552    if (left > maxLeft) left = maxLeft;
553
554    tooltip.style.top = top + 'px';
555    tooltip.style.left = left + 'px';
556    tooltip.classList.add('is-active');
557
558    const iconCenter = rect.left + rect.width / 2;
559    const arrowLeft = iconCenter - left;
560    tooltip.style.setProperty('--tooltip-arrow-left', arrowLeft + 'px');
561}
562
563function hideAllTooltips() {
564    document.querySelectorAll('.tooltip-content.is-active').forEach(el => {
565        el.classList.remove('is-active');
566
567        if (el.parentNode) {
568            el.parentNode.removeChild(el);
569        }
570    });
571
572    document.querySelectorAll('.js-bonus-tooltip').forEach(trigger => {
573        if (trigger._tooltip) {
574            trigger._tooltip = null;
575        }
576    });
577}
578
579function initTooltip(trigger) {
580    if (trigger.dataset.tooltipInited === '1') return;
581    trigger.dataset.tooltipInited = '1';
582
583    trigger.addEventListener('click', e => {
584        e.stopPropagation();
585        showTooltip(trigger);
586    });
587
588    if (!('ontouchstart' in window)) {
589        trigger.addEventListener('mouseenter', () => showTooltip(trigger));
590        trigger.addEventListener('mouseleave', hideAllTooltips);
591    }
592}
593
594function initBonusTooltips() {
595    document.querySelectorAll('.js-bonus-tooltip').forEach(trigger => {
596        initTooltip(trigger);
597    });
598}
599
600function initAllPhoneInputs() {
601  const phoneInputs = document.querySelectorAll('.js-phone-mask');
602  phoneInputs.forEach(initSinglePhoneInput);
603}
604/*
605 * Поле телефона заводится по первому касанию, а не при загрузке страницы.
606 *
607 * Маска и выбор страны живут в двух библиотеках на 173 КБ. Если заводить поле
608 * сразу, они приезжают на каждой странице сайта — а поле телефона в подвале
609 * есть везде, и трогают его единицы. Поэтому ждём касания: кто телефон не
610 * вводит, за него и не платит.
611 *
612 * Исключения, когда заводим сразу:
613 *
614 *  — в поле уже стоит номер: его нужно показать в правильном виде, и коснуться
615 *    поля человек может не собираться;
616 *  — рядом лежит скрытое `NORMALIZED_PHONE`: страница живёт ради номера
617 *    (регистрация, вход), и разобранный номер понадобится ей наверняка.
618 *
619 * На оформлении заказа библиотеки подключены заранее самим шаблоном, поэтому
620 * там эта отсрочка не работает вовсе — поле заводится сразу, как и было.
621 */
622function initSinglePhoneInput(input) {
623  if (!input) return;
624  if (input._iti) return;
625  if (!window.PhoneInput) {
626    return;
627  }
628
629  const container = input.closest('form, .reveal, .order-form, body');
630  const hiddenInput = container
631    ? container.querySelector('input[name="NORMALIZED_PHONE"]')
632    : null;
633
634  const завести = () => window.PhoneInput.init(input, {
635    value: input.value || '',
636    hiddenInput: hiddenInput
637  });
638
639  const библиотекиНаМесте = typeof window.IMask !== 'undefined'
640    && typeof window.intlTelInput !== 'undefined';
641
642  if (библиотекиНаМесте || input.value || hiddenInput) {
643    завести();
644
645    return;
646  }
647
648  input.addEventListener('focus', завести, { once: true });
649}
650
651//функция обновления прогрессивной скидки
652function updatePromoHint(promo) {
653    const $block = $('.js-promo-hint');
654    const $sum = $('.js-promo-hint-sum');
655    const $discount = $('.js-promo-hint-discount');
656    if (!promo) {
657        $block.addClass('hide');
658        return;
659    }
660    $block.removeClass('hide');
661    $sum.text(promo.REMAINING_SUMM);
662    let discountText = promo.DISCOUNT;
663    if (promo.UNIT === 'Perc') {
664        discountText += '%';
665    } if (promo.UNIT === 'Perc') {
666        discountText = promo.DISCOUNT + '%';
667    } else if (promo.UNIT === 'CurEach') {
668        discountText = promo.DISCOUNT + ' ' + знакВалюты() + ' на каждый товар';
669    } else if (promo.UNIT === 'CurAll') {
670        discountText = promo.DISCOUNT + ' ' + знакВалюты() + ' на общую сумму товаров';
671    }
672    $discount.text(discountText);
673}
674// Применяет страну из <select id="inputcountry"> ко всем полям .js-phone-mask
675function syncPhoneInputsFromCountrySelect() {
676  const select = document.getElementById('inputcountry')
677    || document.querySelector('select[name="COUNTRY"]');
678
679  if (!select) return;
680
681  const iso2 = window.PhoneInput.bitrixToIso2(select.value);
682  syncPhoneInputsCountry(iso2);
683}
684
685// Применяет указанную страну ко всем телефонным полям на странице(кроме попапа авторизации)
686function syncPhoneInputsCountry(iso2, options) {
687  options = options || {};
688  const force = options.force === true;
689
690  if (!iso2 || !window.PhoneInput || !window.PhoneInput.lockCountry) return;
691
692  document.querySelectorAll('.js-phone-mask').forEach(function (input) {
693    if (input.closest('#auth')) return;
694    if (input.classList.contains('is-influence')) return;
695    if (!input._iti) return;
696
697    input._iti.promise.then(function () {
698      // Без force если в поле валидный номер другой страны, оставляем его.
699      //
700      // Спрашиваем через PhoneInput, а не у intl-tel-input напрямую: с
701      // отложенной загрузкой утилит его `isValidNumber()` бросает исключение,
702      // а не отвечает «нет». Здесь это обрывало обещание, и страна на поле не
703      // фиксировалась вовсе — поймано в консоли страницы оформления заказа.
704      if (!force && window.PhoneInput.isValid(input)) {
705        const currentCountry = input._iti.getSelectedCountryData();
706        if (currentCountry && currentCountry.iso2) {
707          window.PhoneInput.lockCountry(input, currentCountry.iso2);
708          return;
709        }
710      }
711
712      // С force или если поле пустое лочим на запрошенную
713      window.PhoneInput.lockCountry(input, iso2);
714    });
715  });
716}
717window.initAllPhoneInputs = initAllPhoneInputs;
718window.syncPhoneInputsCountry = syncPhoneInputsCountry;
719window.syncPhoneInputsFromCountrySelect = syncPhoneInputsFromCountrySelect;
720
721
722/**
723 * Отправка цели в рекламный пиксель.
724 *
725 * Проверять только наличие объекта мало: в бою `generalPixel` существует, а
726 * `postClick` у него не функция — каждый клик по «В корзину» ронял
727 * необработанный TypeError. Проверка перенесена в момент отправки ещё и
728 * потому, что пиксель подключается сторонним скриптом и к моменту навешивания
729 * обработчиков может быть не готов: раньше в этом случае цели не отправлялись
730 * вовсе.
731 */
732function trackPixelGoal(code) {
733    if (typeof generalPixel === 'undefined' || typeof generalPixel.postClick !== 'function') {
734        return;
735    }
736
737    generalPixel.postClick(code);
738}
739
740function initPixelGoal(){
741    // Отправка формы Получить скидку
742    $(document).on('click',"#compile-lh-widget form[action='lh_banner'] input.widget--submit", function(e) {
743        let form = $(this).parents('form');
744        let EmailValue = form.find('input[name="email"]').val();
745        let agreementCheckboxValue = form.find('input[name="agreement"]').is(':checked');
746        let errorWidgetText = form.find('.widget--error_text');
747        if(EmailValue && EmailValue != '' && agreementCheckboxValue && errorWidgetText.length != 0 && errorWidgetText.hasClass('visibility_none')){
748            trackPixelGoal('a1');
749        }
750    });
751    // Кнопка "В корзину"
752    $(document).on('click','.product-card [data-action="item-card-add"]', function(e) {
753        trackPixelGoal('a2');
754    });
755    // Кнопка "Купить в 1 клик"
756    $(document).on('click','.product-card .product-btn.product-btn_buy', function(e) {
757        trackPixelGoal('a3');
758    });
759    // Кнопка "Пройти тест"
760    $(document).on('click','.menu-dropdown__items .test__btn', function(e) {
761        trackPixelGoal('a4');
762    });
763}
764
765// Закрытие окна видеоотзыва крестиком. Открывающий код лежит в собранном
766// main.js и умеет закрывать окно только кликом по подложке и по Esc: на
767// телефоне видео занимает почти весь экран, а клавиатуры нет — выйти из
768// просмотра было нечем.
769function initVideoReviewModalClose() {
770    var $overlay = $('.js-modal-video-review');
771
772    if ($overlay.length === 0) {
773        return;
774    }
775
776    $overlay.on('click', '.js-modal-video-close', function (e) {
777        e.preventDefault();
778
779        var video = $overlay.find('.js-modal-video')[0];
780
781        $overlay.removeClass('active');
782
783        // Пауза и сброс источника — после анимации скрытия, иначе видно,
784        // как кадр пропадает раньше самого окна.
785        setTimeout(function () {
786            if (video) {
787                video.pause();
788                video.removeAttribute('src');
789                video.load();
790            }
791
792            $('body').removeClass('over');
793        }, 300);
794    });
795}
796
797
798// Кнопка корзины в нижнем меню ведёт на страницу корзины, а окно поверх неё
799// открывает Foundation. Переход отменяем только тогда, когда окно реально
800// готово открыться: если тяжёлый бандл со скриптами не доехал — а на плохой
801// связи и через VPN так бывает, — нажатие уводит на страницу, вместо того
802// чтобы не делать ничего. У остальных пунктов меню ссылки есть, поэтому
803// «не работает только корзина» и выглядело загадкой.
804function initCartLinkFallback() {
805    document.addEventListener('click', function (event) {
806        var link = event.target.closest ? event.target.closest('.hero-nav__link_cart') : null;
807
808        if (!link) {
809            return;
810        }
811
812        if (!isModalReady('cart')) {
813            return;
814        }
815
816        event.preventDefault();
817        openCartModal(link.getAttribute('href') || '#cart');
818    });
819}
820
821/*
822 * Окно корзины открываем сами, а не надеемся, что Foundation поймает нажатие.
823 *
824 * Раньше мы только отменяли переход по ссылке, а открыть окно должен был
825 * обработчик Foundation, навешенный на документ через `data-open`. Между
826 * нажатием и этим обработчиком стоит вся цепочка всплытия события, и любой
827 * `stopPropagation` по дороге её обрывает: переход уже отменён, а окно не
828 * открылось — на экране не происходит ничего. Владелец наблюдал это на айфоне
829 * 6 сентября 2026, при том что остальные пункты меню работали: у них обычные
830 * ссылки, отменять там нечего.
831 *
832 * Теперь зовём открытие напрямую. И проверяем результат: если через полсекунды
833 * окна на экране нет — переходим по ссылке пункта меню. Она ведёт на «#cart», а
834 * у окна стоит `data-deep-link="true"`, то есть якорь открывает его же, только
835 * другим путём. Уводить на /personal/cart/ нельзя: та страница свёрстана иначе
836 * и показывает не ту корзину.
837 */
838function openCartModal(href) {
839    var окно = document.getElementById('cart');
840
841    var видно = function () {
842        return окно && getComputedStyle(окно).display !== 'none';
843    };
844
845    try {
846        if (!видно()) {
847            jQuery(окно).foundation('open');
848        }
849    } catch (e) {
850        window.location.href = href;
851
852        return;
853    }
854
855    setTimeout(function () {
856        if (!видно()) {
857            window.location.href = href;
858        }
859    }, 500);
860}
861
862/** Готово ли модальное окно: есть в разметке и поднято Foundation. */
863function isModalReady(id) {
864    var modal = document.getElementById(id);
865
866    if (!modal || !window.jQuery || !jQuery.fn || !jQuery.fn.foundation) {
867        return false;
868    }
869
870    return !!jQuery(modal).data('zfPlugin');
871}
872
873/**
874 * Знак валюты магазина: «₽» на российском сайте, «₸» на казахстанском.
875 *
876 * Печатает шаблон в `window.WAU_CURRENCY_SIGN`: файлы у обоих магазинов общие,
877 * и рублёвый знак на витрине, торгующей в тенге, просто неверен.
878 */
879function знакВалюты() {
880    return window.WAU_CURRENCY_SIGN || '₽';
881}

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.