PageSourceSearch

https://pass24online.ru/wp-content/themes/pass24-child/assets/js/exit-intent.js?v=1791292815

js pass24online.ru collected 2026-10-08 14:15:58 UTC 14,624 bytes, 319 lines download raw bytes

1/**
2 * Exit-intent popup — PASS24
3 * Triggers:
4 *   1. mouseout to top of page (desktop) — only after DELAY_MS
5 *   2. After DELAY_MS (15s) on all pages
6 * Shows once per 7 days (cookie) — при согласии на функциональные cookie;
7 * без него попап показывается один раз за загрузку страницы и ничего не хранит.
8 * Does NOT fire before delay.
9 * Не показывается на мобильном (см. MOBILE_MAX_WIDTH).
10 */
11(function () {
12  'use strict';
13
14  var COOKIE_NAME  = 'p24_exit_shown';
15  var COOKIE_DAYS  = 7;
16  var DELAY_MS     = 15000;
17
18  /**
19   * Порог, ниже которого попап не показываем. На узком экране он закрывает
20   * весь вьюпорт — это intrusive interstitial по классификации Google, риск
21   * для мобильного ранжирования. Первый триггер (mouseleave) на мобильном и
22   * так не срабатывает, поэтому отключается только таймерный показ.
23   */
24  var MOBILE_MAX_WIDTH = 767;
25
26  function isMobileViewport() {
27    return window.matchMedia('(max-width: ' + MOBILE_MAX_WIDTH + 'px)').matches;
28  }
29
30  function getCookie(name) {
31    var match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
32    return match ? match[2] : '';
33  }
34
35  /**
36   * Запомнили ли показ в этой загрузке страницы, когда cookie писать нельзя.
37   *
38   * Ограничение частоты («раз в 7 дней») — функциональная cookie, и без
39   * согласия на функциональные она не пишется (SITE-21). Совсем отключать
40   * попап из-за этого незачем: он часть сайта, а не сторонний сервис и не
41   * сетевой запрос. Поэтому при отказе память сжимается до одной загрузки
42   * страницы — попап покажется один раз и ничего не сохранит. Если согласие
43   * придёт уже после показа, память запишется тогда же (см. markShown).
44   */
45  var shownThisPage = false;
46
47  function storageAllowed() {
48    return !!(window.P24Consent && window.P24Consent.allowed('functional'));
49  }
50
51  function alreadyShown() {
52    if (shownThisPage) {
53      return true;
54    }
55    return storageAllowed() && getCookie(COOKIE_NAME) === '1';
56  }
57
58  /**
59   * Запомнить показ.
60   *
61   * Запись идёт через единственную дверь хранилища (SITE-27): категория
62   * объявлена литералом на месте записи и проверяется в момент записи. Своего
63   * setCookie() у модуля больше нет — забыть проверку стало нечем.
64   *
65   * Сама запись ждёт решения через whenAllowed(). Попап показывается на 15-й
66   * секунде, то есть обычно раньше, чем человек ответит баннеру, и разовая
67   * попытка записи в этот момент упиралась бы в отказ двери навсегда: согласие,
68   * данное следом, её уже не повторяет, и ограничение «раз в 7 дней» молча
69   * превращалось бы в «каждую загрузку страницы» у всех, кто нажал «Принять»
70   * после показа. При отказе fn не выполняется вовсе, а проверка категории
71   * внутри двери остаётся на месте — whenAllowed() её не заменяет.
72   */
73  function markShown() {
74    shownThisPage = true;
75    if (window.P24Consent) {
76      window.P24Consent.whenAllowed('functional', function () {
77        window.P24Consent.set(COOKIE_NAME, '1', COOKIE_DAYS, 'functional');
78      });
79    }
80  }
81
82  function isProductOrSolutionPage() {
83    var p = window.location.pathname;
84    return p.indexOf('/products/') === 0 || p.indexOf('/solutions/') === 0;
85  }
86
87  function el(tag, cls, txt) {
88    var e = document.createElement(tag);
89    if (cls) e.className = cls;
90    if (txt !== undefined) e.textContent = txt;
91    return e;
92  }
93
94  function buildDemoVariant(box, close) {
95    var badge = el('div', 'p24-exit-popup__badge', 'Бесплатно');
96    var title = el('h2', 'p24-exit-popup__title', 'Посмотрите PASS24 вживую');
97    var text  = el('p', 'p24-exit-popup__text', '30-минутная демонстрация под ваш объект — без обязательств.');
98    var cta   = el('a', 'p24-btn p24-btn-primary p24-exit-popup__cta', 'Записаться на демо');
99    cta.href  = '/demo/';
100    var skip  = el('button', 'p24-exit-popup__skip', 'Нет, спасибо');
101    skip.addEventListener('click', close);
102    box.appendChild(badge);
103    box.appendChild(title);
104    box.appendChild(text);
105    box.appendChild(cta);
106    box.appendChild(skip);
107  }
108
109  function buildChecklistVariant(box, close) {
110    var badge = el('div', 'p24-exit-popup__badge', 'Бесплатный гайд');
111    var title = el('h2', 'p24-exit-popup__title', 'Скачайте чек-лист бесплатно');
112    var text  = el('p', 'p24-exit-popup__text', '15 пунктов для выбора облачной СКУД — экономия 200+ часов на поиске.');
113
114    var form  = el('form', 'p24-exit-popup__form');
115    var input = el('input', 'p24-form-input');
116    input.type        = 'email';
117    input.placeholder = 'Ваш email';
118    input.required    = true;
119
120    var btn   = el('button', 'p24-btn p24-btn-primary', 'Получить чек-лист');
121    btn.type  = 'submit';
122
123    var consentLabel = el('label', 'p24-form-consent');
124    var consentBox   = document.createElement('input');
125    consentBox.type  = 'checkbox';
126    consentBox.name  = 'consent';
127    consentBox.required = true;
128    var consentSpan  = document.createElement('span');
129    var consentLink  = document.createElement('a');
130    consentLink.href = '/privacy/';
131    consentLink.target = '_blank';
132    consentLink.textContent = 'согласие на\u00A0обработку персональных данных';
133    consentSpan.appendChild(document.createTextNode('Я\u00A0даю '));
134    consentSpan.appendChild(consentLink);
135    consentLabel.appendChild(consentBox);
136    consentLabel.appendChild(consentSpan);
137
138    // Необязательное согласие на рекламн
138ые сообщения (SITE-24). Отдельная цель
139    // — отдельный чекбокс: он не required, не отмечен заранее и к состоянию
140    // кнопки не привязан, отказ от рекламы отправке заявки не мешает.
141    var adsLabel = el('label', 'p24-form-marketing');
142    var adsBox   = document.createElement('input');
143    adsBox.type  = 'checkbox';
144    adsBox.name  = 'marketing_consent';
145    adsBox.value = '1';
146    adsBox.setAttribute('data-p24-marketing-consent', '');
147    var adsSpan  = document.createElement('span');
148    var adsLink  = document.createElement('a');
149    adsLink.href   = '/marketing-consent/';
150    adsLink.target = '_blank';
151    adsLink.rel    = 'noopener';
152    adsLink.textContent = 'согласие';
153    adsSpan.appendChild(document.createTextNode('Я\u00A0даю '));
154    adsSpan.appendChild(adsLink);
155    adsSpan.appendChild(document.createTextNode(
156      ' на\u00A0получение рассылки рекламно-информационных материалов'
157    ));
158    adsLabel.appendChild(adsBox);
159    adsLabel.appendChild(adsSpan);
160
161    // Honeypot: скрытое поле-ловушка для ботов (единый паттерн p24-hp).
162    var honeypot = el('input', 'p24-hp');
163    honeypot.type = 'text';
164    honeypot.name = 'website';
165    honeypot.tabIndex = -1;
166    honeypot.setAttribute('autocomplete', 'off');
167    honeypot.setAttribute('aria-hidden', 'true');
168
169    form.appendChild(honeypot);
170    form.appendChild(input);
171    form.appendChild(consentLabel);
172    form.appendChild(adsLabel);
173    form.appendChild(btn);
174
175    // Кнопка выключена, пока не отмечено согласие на ПДн, и гаснет обратно при
176    // снятии флажка (assets/js/consent-gate.js).
177    var consentGate = window.P24ConsentGate.bind(consentBox, btn);
178
179    var successDiv = el('div', 'p24-exit-popup__success');
180    successDiv.style.display = 'none';
181    var successText = el('p', '', 'Отправлено! Проверьте почту.');
182    var successLink = el('a', 'p24-btn p24-btn-secondary', 'Открыть чек-лист');
183    successLink.href   = '/resources/guide-choose-skud/';
184    successLink.target = '_blank';
185    successDiv.appendChild(successText);
186    successDiv.appendChild(successLink);
187
188    form.addEventListener('submit', function (e) {
189      e.preventDefault();
190      var email = input.value.trim();
191      if (!email) return;
192
193      // Гейт согласия на ПДн. Форма настоящая, required браузер проверяет,
194      // а кнопка до этого момента выключена, но проверка дублируется здесь:
195      // попап живёт на всех страницах, и без явного согласия почта не должна
196      // уходить в CRM ни при каких условиях.
197      if (!consentGate.isGiven()) {
198        consentLabel.classList.add('has-error');
199        consentBox.focus();
200        return;
201      }
202      consentLabel.classList.remove('has-error');
203
204      // Гейт занимаем на время запроса: он же защита от двойного сабмита и от
205      // «оживления» кнопки, если флажок снимут и поставят обратно во время fetch.
206      if (!consentGate.beginSubmit()) return;
207      btn.textContent = 'Отправляем...';
208
209      var data = {
210        email:   email,
211        source:  'exit_intent_checklist',
212        consent: true,
213        // Рекламное согласие читается из чекбокса, а не задаётся константой:
214        // «да» здесь может поставить только человек (SITE-24).
215        marketing_consent: window.P24Marketing.isGiven(form),
216        website: honeypot.value
217      };
218      window.P24Analytics.enrichFormData(data, function (enriched) {
219        fetch(wpApiSettings.root + 'pass24/v1/download-lead', {
220          method:  'POST',
221          headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': wpApiSettings.nonce },
222          body:    JSON.stringify(enriched),
223        })
224          .then(function (response) {
225            if (!response.ok) throw new Error('Server error');
226            form.style.display        = 'none';
227            successDiv.style.display  = 'block';
228            if (typeof ym !== 'undefined') {
229              ym(108384915, 'reachGoal', 'exit_intent_lead');
230              ym(108384915, 'reachGoal', 'generate_lead');
231            }
232          })
233          .catch(function () {
234            // Не `disabled = false`: снятое согласие обязано оставить кнопку
235            // выключенной и после неудачной отправки.
236            consentGate.endSubmit();
237            btn.textContent = 'Получить чек-лист';
238          });
239      });
240    });
241
242    var skip = el('button', 'p24-exit-popup__skip', 'Нет, спасибо');
243    skip.addEventListener('click', close);
244
245    box.appendChild(badge);
246    box.appendChild(title);
247    box.appendChild(text);
248    box.appendChild(form);
249    box.appendChild(successDiv);
250    box.appendChild(skip);
251  }
252
253  function showPopup() {
254    // Проверка и здесь, а не только на старте: ширина могла измениться
255    // (поворот экрана, ресайз окна) за 15+ секунд до срабатывания таймера.
256    if (isMobileViewport()) return;
257    if (alreadyShown()) return;
258    markShown();
259
260    var variant = isProductOrSolutionPage() ? 'demo' : 'checklist';
261
262    var popup   = el('div', 'p24-exit-popup');
263    popup.setAttribute('role', 'dialog');
264    popup.setAttribute('aria-modal', 'true');
265
266    var overlay = el('div', 'p24-exit-popup__overlay');
267    var box     = el('div', 'p24-exit-popup__box');
268    var closeBtn = el('button', 'p24-exit-popup__close', '\u00D7');
269    closeBtn.setAttribute('aria-label', 'Закрыть');
270
271    function close() {
272      popup.classList.remove('is-visible');
273      setTimeout(function () { if (popup.parentNode) popup.parentNode.removeChild(popup); }, 300);
274    }
275
276    closeBtn.addEventListener('click', close);
277    overlay.addEventListener('click', close);
278    document.addEventListener('keydown', function escHandler(e) {
279      if (e.key === 'Escape') { close(); document.removeEventListener('keydown', escHandler); }
280    });
281
282    box.appendChild(closeBtn);
283
284    if (variant === 'demo') {
285      buildDemoVariant(box, close);
286    } else {
287      buildChecklistVariant(box, close);
288    }
289
290    popup.appendChild(overlay);
291    popup.appendChild(box);
292    document.body.appendChild(popup);
293
294    requestAnimationFrame(function () {
295      requestAnimationFrame(function () { popup.classList.add('is-visible'); });
296    });
297  }
298
299  document.addEventListener('DOMContentLoaded', function () {
300    if (alreadyShown()) return;
301    // На мобильном не заводим таймеры вообще — попап там не показывается.
302    if (isMobileViewport()) return;
303
304    var delayPassed = false;
305
306    // Enable triggers only after delay
307    setTimeout(function () {
308      delayPassed = true;
309    }, DELAY_MS);
310
311    // Trigger 1: exit intent (desktop — mouse leaves viewport from top, only after delay)
312    document.addEventListener('mouseleave', function (e) {
313      if (delayPassed && e.clientY <= 0) showPopup();
314    });
315
316    // Trigger 2: timed (shows after delay + 15s extra if user hasn't left)
317    setTimeout(showPopup, DELAY_MS + 15000);
318  });
319})();

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.