PageSourceSearch

https://goroddorog-avto.ru/local/chat-widget/exit-intent.js?v=4

js goroddorog-avto.ru collected 2026-10-08 07:54:07 UTC 9,331 bytes, 216 lines download raw bytes

1/* =============================================
2   EXIT-INTENT POPUP — Город Дорог
3   Desktop: mouseleave вверх
4   Mobile:  visibilitychange / back-scroll
5   Кулдаун: 72ч (localStorage)
6   Мин. время на странице: 10 сек
7   Форма: интегрирована с form.js (.nbt + [action])
8   ============================================= */
9
10(function () {
11    'use strict';
12
13    var STORAGE_KEY = 'gd_exit_intent';
14    var COOLDOWN_MS = 72 * 60 * 60 * 1000;
15    var MIN_TIME_MS = 10000;
16    var pageLoadTime = Date.now();
17    var shown = false;
18
19    function isOnCooldown() {
20        try {
21            var ts = localStorage.getItem(STORAGE_KEY);
22            if (!ts) return false;
23            return (Date.now() - parseInt(ts, 10)) < COOLDOWN_MS;
24        } catch (e) { return false; }
25    }
26
27    function setCooldown() {
28        try { localStorage.setItem(STORAGE_KEY, Date.now().toString()); }
29        catch (e) { /* ok */ }
30    }
31
32    function hasEnoughTime() {
33        return (Date.now() - pageLoadTime) >= MIN_TIME_MS;
34    }
35
36    /* Подтягиваем актуальную дату акции из hero на странице.
37       Ищем «до DD <месяц>» — на главной это .nbanner__title (динамика из админки),
38       на других страницах — любой текст с этой формой. Нормализуем в «Только до DD <месяц>». */
39    function getSaleDeadline() {
40        var monthRe = /до\s+(\d{1,2})\s+(января|февраля|марта|апреля|мая|июня|июля|августа|сентября|октября|ноября|декабря)/i;
41
42        // 1. Приоритет — главный hero (.nbanner__title)
43        var hero = document.querySelector('.nbanner__title');
44        if (hero) {
45            var m = (hero.textContent || '').match(monthRe);
46            if (m) return 'Только до ' + m[1] + ' ' + m[2].toLowerCase();
47        }
48
49        // 2. Fallback — любой leaf-узел в DOM с подходящей датой
50        var nodes = document.querySelectorAll('b, strong, span, div, p, h1, h2, h3');
51        for (var i = 0; i < nodes.length; i++) {
52            if (nodes[i].children.length > 0) continue;
53            var txt = (nodes[i].textContent || '').replace(/\s+/g, ' ').trim();
54            var mm = txt.match(monthRe);
55            if (mm) return 'Только до ' + mm[1] + ' ' + mm[2].toLowerCase();
56        }
57
58        // Единый источник даты — наш оверрайд в init.php: зеркало клиентского
59        // getSaleDate() (тот же /local/date.log и тот же шаг P2W), поэтому дата
60        // здесь не может разойтись с датой на главной. Хардкод — только на случай,
61        // если инъекция не доехала.
62        return 'Только до ' + (window.__gdSaleDeadline || '27 августа');
63    }
64
65    function createPopup() {
66        var currentLink = window.location.href;
67        var saleDeadline = getSaleDeadline();
68        var overlay = document.createElement('div');
69        overlay.className = 'ei-overlay';
70        overlay.id = 'exitIntentPopup';
71        overlay.innerHTML =
72            '<div class="ei-card">' +
73                '<div class="ei-photo">' +
74                    '<img src="/local/chat-widget/img/exit-intent-photo.jpg" alt="Город Дорог — получите права" loading="lazy">' +
75                '</div>' +
76                '<div class="ei-content">' +
77                    '<button class="ei-close" aria-label="Закрыть">&times;</button>' +
78                    '<div class="ei-badge">' + saleDeadline + '</div>' +
79                    '<h2 class="ei-title">Скидка 7&nbsp;000&nbsp;₽ на&nbsp;обучение</h2>' +
80                    '<ul class="ei-list">' +
81                        '<li>Права за 3 месяца</li>' +
82                        '<li>Без доплат за бензин</li>' +
83                        '<li>Сопровождаем до получения прав</li>' +
84                    '</ul>' +
85                    '<div class="ei-form" id="ei-form" action="/">' +
86                        '<input type="text" name="user_name" placeholder="Ваше имя" autocomplete="off" required>' +
87                        '<input type="tel" name="MESSAGE" placeholder="+7 (___) ___-__-__" class="js-mask-phone" autocomplete="off" required>' +
88                        '<input type="hidden" name="user_email" value="">' +
89                        '<input type="hidden" name="recaptcha_token" value="">' +
90                        '<input type="hidden" name="grec" value="test">' +
91                        '<input type="hidden" name="AJAX" value="Y">' +
92                        '<input type="hidden" name="action" value="form">' +
93                        '<input type="hidden" name="goal" value="form_trial_lesson">' +
94                        '<input type="hidden" name="FORM_NAME" value="Скидка (exit-intent)">' +
95                        '<input type="hidden" name="CURRENT_LINK" value="' + currentLink + '">' +
96                        '<div class="result"></div>' +
97                        '<div class="nbt ei-btn">Получить скидку</div>' +
98                    '</div>' +
99                    '<p class="ei-privacy">Нажимая на кнопку, я соглашаюсь с условиями<br>' +
100                        '<a href="/politika-konfidentsialnosti/" target="_blank">Политики конфиденциальности</a></p>' +
101                '</div>' +
102            '</div>';
103
104        document.body.appendChild(overlay);
105
106        // Маска телефона — если есть jQuery + maskedinput
107        setTimeout(function () {
108            try {
109                var phoneInput = overlay.querySelector('input[type="tel"]');
110                if (phoneInput && window.jQuery && jQuery.fn.mask) {
111                    jQuery(phoneInput).mask('+7 (999) 999-99-99');
112                }
113            } catch (e) { /* ok */ }
114        }, 100);
115
116        return overlay;
117    }
118
119    function showPopup() {
120        if (shown || isOnCooldown() || !hasEnoughTime()) return;
121        shown = true;
122        setCooldown();
123
124        var overlay = document.getElementById('exitIntentPopup') || createPopup();
125
126        overlay.style.display = 'flex';
127        overlay.offsetHeight;
128        overlay.classList.add('ei-overlay--visible');
129
130        // Re-init form.js handlers for new .nbt button
131        if (typeof initEventClick === 'function') {
132            initEventClick();
133        }
134
135        var closeBtn = overlay.querySelector('.ei-close');
136        if (closeBtn) {
137            closeBtn.addEventListener('click', function () { hidePopup(overlay); });
138        }
139
140        overlay.addEventListener('click', function (e) {
141            if (e.target === overlay) hidePopup(overlay);
142        });
143
144        document.addEventListener('keydown', function onEsc(e) {
145            if (e.key === 'Escape') {
146                hidePopup(overlay);
147                document.removeEventListener('keydown', onEsc);
148            }
149        });
150    }
151
152    function hidePopup(overlay) {
153        overlay.classList.remove('ei-overlay--visible');
154        setTimeout(function () {
155            overlay.style.display = 'none';
156        }, 350);
157    }
158
159    // --- Desktop: mouseleave top ---
160    function initDesktop() {
161        document.documentElement.addEventListener('mouseleave', function (e) {
162            if (e.clientY < 0) showPopup();
163        });
164    }
165
166    // --- Mobile: visibility + back-scroll ---
167    function initMobile() {
168        document.addEventListener('visibilitychange', function () {
169            if (document.visibilityState === 'hidden') {
170                if (!shown && !isOnCooldown() && hasEnoughTime()) {
171                    document.addEventListener('visibilitychange', function onReturn() {
172                        if (document.visibilityState === 'visible') {
173                            document.removeEventListener('visibilitychange', onReturn);
174                            setTimeout(showPopup, 500);
175                        }
176                    });
177                }
178            }
179        });
180
181        var lastScrollY = window.scrollY;
182        var scrollUpCount = 0;
183
184        window.addEventListener('scroll', function () {
185            var currentY = window.scrollY;
186            if (currentY < lastScrollY && currentY < 100 && lastScrollY > 300) {
187                scrollUpCount++;
188                if (scrollUpCount >= 2) showPopup();
189            } else if (currentY > lastScrollY) {
190                scrollUpCount = 0;
191            }
192            lastScrollY = currentY;
193        }, { passive: true });
194    }
195
196    function init() {
197        if (isOnCooldown()) return;
198
199        createPopup();
200
201        var isMobile = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent) ||
202                       window.innerWidth < 768;
203
204        if (isMobile) {
205            initMobile();
206        } else {
207            initDesktop();
208        }
209    }
210
211    if (document.readyState === 'loading') {
212        document.addEventListener('DOMContentLoaded', init);
213    } else {
214        init();
215    }
216})();

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.