PageSourceSearch

https://dolphin-anty.info/wp-content/themes/blog/app/js/onload/form.js

js dolphin-anty.info collected 2026-10-02 21:22:29 UTC 29,601 bytes, 746 lines download raw bytes

1    // Защита от повторной загрузки скрипта
2    if (window.formJsLoaded) {
3        // Скрипт уже загружен, выходим
4        if (typeof window.globalIntlTelInit === 'undefined') {
5            window.globalIntlTelInit = false;
6        }
7        // Проверяем, есть ли уже функции
8        if (typeof window.initTelNumber === 'function' && typeof window.initForm === 'function') {
9
10        }
11    }
12
13// Исправление проблемы с Beget: установка куки beget=begetok для работы REST API
14(function() {
15    // Устанавливаем куку при загрузке страницы
16    function setBegetCookie() {
17        // Проверяем, не установлена ли уже кука
18        if (document.cookie.indexOf('beget=begetok') === -1) {
19            try {
20                const domain = window.location.hostname;
21                // Для поддоменов (например, wiki-test.programstore.ru) устанавливаем на родительский домен
22                let cookieString = 'beget=begetok; path=/; max-age=31536000';
23                
24                // Если это поддомен (содержит точку и не localhost), устанавливаем на родительский домен
25                if (domain.includes('.') && domain !== 'localhost' && !domain.startsWith('127.')) {
26                    const parts = domain.split('.');
27                    if (parts.length > 2) {
28                        // Для wiki-test.programstore.ru используем .programstore.ru
29                        const parentDomain = '.' + parts.slice(-2).join('.');
30                        cookieString += '; domain=' + parentDomain;
31                    }
32                }
33                
34                document.cookie = cookieString;
35            } catch (e) {
36                // Если не удалось установить с domain, пробуем без него
37                try {
38                    document.cookie = 'beget=begetok; path=/; max-age=31536000';
39                } catch (e2) {
40                    console.warn('Не удалось установить куку beget:', e2);
41                }
42            }
43        }
44    }
45
46    // Устанавливаем куку сразу
47    setBegetCookie();
48
49    // Также устанавливаем при готовности DOM (на случай, если скрипт загрузился раньше)
50    if (document.readyState === 'loading') {
51        document.addEventListener('DOMContentLoaded', setBegetCookie);
52    }
53
54    // Перехватываем fetch для добавления куки в запросы
55    const originalFetch = window.fetch;
56    window.fetch = function(...args) {
57        // Убеждаемся, что кука установлена перед запросом
58        setBegetCookie();
59        
60        // Если это запрос к REST API, убеждаемся что кука отправляется
61        const url = typeof args[0] === 'string' ? args[0] : args[0].url;
62        if (url && (url.includes('/wp-json/') || url.includes('/wp-admin/admin-ajax.php'))) {
63            // Устанавливаем credentials для отправки кук
64            if (args[1]) {
65                args[1].credentials = args[1].credentials || 'same-origin';
66            } else {
67                args[1] = { credentials: 'same-origin' };
68            }
69        }
70        
71        return originalFetch.apply(this, args);
72    };
73
74    // Перехватываем XMLHttpRequest для добавления куки в запросы
75    const originalXHROpen = XMLHttpRequest.prototype.open;
76    const originalXHRSend = XMLHttpRequest.prototype.send;
77    
78    XMLHttpRequest.prototype.open = function(method, url, ...rest) {
79        this._url = url;
80        return originalXHROpen.apply(this, [method, url, ...rest]);
81    };
82    
83    XMLHttpRequest.prototype.send = function(...args) {
84        // Убеждаемся, что кука установлена перед запросом
85        setBegetCookie();
86        
87        // Если это запрос к REST API, убеждаемся что кука отправляется
88        if (this._url && (this._url.includes('/wp-json/') || this._url.includes('/wp-admin/admin-ajax.php'))) {
89            // withCredentials уже должен быть установлен по умолчанию для same-origin запросов
90        }
91        
92        return originalXHRSend.apply(this, args);
93    };
94})();
95
96// Инициализация intl-tel-input для полей телефона
97if (typeof window.globalIntlTelInit === 'undefined') {
98    window.globalIntlTelInit = false;
99}
100
101// Собственная реализация inputmask без jQuery
102const createInputMask = (input, mask, options = {}) => {
103    const opts = { placeholder: "X", clearMaskOnLostFocus: true, ...options };
104    
105    // Удаляем старые обработчики
106    if (input._maskHandler) {
107        ['input', 'focus', 'blur'].forEach(event => 
108            input.removeEventListener(event, input[`_${event}Handler`])
109        );
110    }
111    
112    // Обработчик ввода
113    input._inputHandler = e => {
114        const value = e.target.value.replace(/\D/g, '');
115        let result = '', maskIndex = 0, valueIndex = 0;
116        
117        while (maskIndex < mask.length && valueIndex < value.length) {
118            result += mask[maskIndex] === '9' ? value[valueIndex++] : mask[maskIndex];
119            maskIndex++;
120        }
121        e.target.value = result;
122    };
123    
124    // Обработчики фокуса и потери фокуса
125    input._focusHandler = e => {
126        if (e.target.value === '') e.target.value = mask.replace(/9/g, opts.placeholder);
127    };
128    
129    input._blurHandler = e => {
130        if (opts.clearMaskOnLostFocus) {
131            const value = e.target.value.replace(new RegExp(opts.placeholder, 'g'), '');
132            if (value === mask.replace(/9/g, '')) e.target.value = '';
133        }
134    };
135    
136    // Добавляем обработчики
137    ['input', 'focus', 'blur'].forEach(event => 
138        input.addEventListener(event, input[`_${event}Handler`])
139    );
140    
141    input.setAttribute('placeholder', mask.replace(/9/g, opts.placeholder));
142    return input;
143};
144
145// Глобальные функции для совместимости
146window.createInputMask = createInputMask;
147window.applyInputMask = createInputMask;
148
149// Инициализация телефонных полей
150const initTelNumber = () => {
151    if (window.globalIntlTelInit || typeof window.intlTelInput !== 'function') return;
152    
153    const telInputs = document.querySelectorAll('form input[type="tel"]');
154    if (!telInputs.length) return;
155    
156    const countryFormats = {
157        'by': 'XX XXX-XX-XX', 'uz': 'XX XXX-XX-XX', 'tj': 'XX XXX-XX-XX', 'tm': 'XX XXX-XX-XX',
158        'am': 'XX XXX-XX-XX', 'az': 'XX XXX-XX-XX', 'ge': 'XX XXX-XX-XX',
159        'kz': 'XXX XXX-XX-XX', 'kg': 'XXX XXX-XX-XX', 'md': 'XXX XXX-XX-XX',
160        'ru': '(XXX) XXX-XX-XX'
161    };
162    
163    let phonePlaceholder = countryFormats.ru;
164    
165    telInputs.forEach(input => {
166        if (input._iti) return;
167        
168        const iti = window.intlTelInput(input, {
169            autoPlaceholder: "aggressive",
170            autoHideDialCode: true,
171            nationalMode: true,
172            separateDialCode: true,
173            initialCountry: "ru",
174            onlyCountries: ["ru", "kz", "by", "uz", "am", "az", "ge", "kg", "md", "tj", "tm"],
175            allowDropdown: true,
176            dropdownContainer: document.body,
177            customPlaceholder: () => phonePlaceholder,
178            utilsScript: window.location.origin + "/wp-content/themes/equeo/app/js/vendor/utils.js",
179        });
180
181        input._iti = iti;
182        input.setAttribute("placeholder", phonePlaceholder);
183        createInputMask(input, phonePlaceholder.replace(/X/g, "9"));
184
185        // Обработчики событий
186        ['focus', 'click'].forEach(event => {
187            input.addEventListener(event, () => {
188                setTimeout(() => {
189                    const pl = input.getAttribute("placeholder") + "";
190                    const res = pl.replace(/X/g, "9");
191                    if (res !== "undefined") {
192                        createInputMask(input, res);
193                    }
194                }, 0);
195            });
196        });
197
198        input.addEventListener('countrychange', () => {
199            input.value = '';
200            const countryData = iti.getSelectedCountryData();
201            phonePlaceholder = countryFormats[countryData.iso2] || countryFormats.ru;
202            
203            input.setAttribute("placeholder", phonePlaceholder);
204            createInputMask(input, phonePlaceholder.replace(/X/g, "9"));
205            
206            const form = input.closest('form');
207            const countryCodeInput = form?.querySelector('[name="country_code"]');
208            if (countryCodeInput) countryCodeInput.value = countryData.dialCode;
209        });
210    });
211    
212    // Обработчики для скрытых полей кода страны
213    document.querySelectorAll('.iti--allow-dropdown').forEach(element => {
214        element.addEventListener('focusout', () => {
215            const dialCode = window.innerWidth < 768 
216                ? element.querySelector('.iti__selected-dial-code')?.innerText
217                : element.querySelector('.iti__active .iti__dial-code')?.dataset.dialCode;
218            
219            if (dialCode) {
220                document.querySelectorAll('#CountryCode,#CountryCode1,#CountryCode2,#CountryCode3')
221                    .forEach(input => input.value = dialCode);
222            }
223        });
224    });
225    
226            window.globalIntlTelInit = true;
227};
228
229// Основная функция инициализации
230const initForm = () => {
231    // Настройка стилей intl-tel-input
232    document.querySelectorAll('.iti').forEach(element => {
233        element.style.display = 'block';
234        element.style.width = '100%';
235    });
236
237    // Инициализация с проверками
238    let intlTelInit = false;
239    const initInterval = setInterval(() => {
240        if (typeof window.intlTelInput === 'function' && !intlTelInit) {
241            initTelNumber();
242            intlTelInit = true;
243        }
244    }, 500);
245
246    // Дополнительная инициализация
247    setTimeout(() => {
248        if (!intlTelInit && typeof window.intlTelInput === 'function') {
249            initTelNumber();
250        }
251    }, 3000);
252};
253
254// Глобальные функции
255window.initTelNumber = initTelNumber;
256window.initForm = initForm;
257
258// Инициализация при готовности DOM
259document.readyState === 'loading' 
260    ? document.addEventListener('DOMContentLoaded', initForm)
261    : initForm();
262
263// Дополнительная инициализация
264        setTimeout(() => !window.globalIntlTelInit && initForm(), 500);
265
266document.addEventListener('submit', e => {
267    if (e.target.classList.contains('wpcf7-form')) {
268        const submitButton = e.target.querySelector('[type=submit]');
269        if (submitButton) {
270            submitButton.disabled = true;
271            submitButton.dataset.originalText = submitButton.textContent;
272            submitButton.textContent = 'Отправка…';
273        }
274    }
275});
276
277document.addEventListener('wpcf7submit', e => {
278    const submitButton = e.target.querySelector('[type=submit]');
279    if (submitButton) {
280        submitButton.disabled = false;
281        if (submitButton.dataset.originalText) {
282            submitButton.textContent = submitButton.dataset.originalText;
283        }
284    }
285});
286
287// Валидация полей
288document.querySelectorAll('.wpcf7-form input[type="text"]').forEach(el => {
289    el.addEventListener('input', () => {
290        el.value = el.value.replace(/[^A-Za-zА-Яа-яЁё\s\-]/g, '');
291    });
292});
293
294document.querySelectorAll('.wpcf7-form input[type="email"]').forEach(el => {
295    el.addEventListener('input', () => {
296        el.value = el.value.replace(/[^a-zA-Z0-9@._\-\+]/g, '');
297    });
298});
299
300// Закрытие попапа
301const closePopup = (formElement) => {
302    const popup = formElement?.closest('.popUp-box');
303    
304    if (popup) {
305        popup.classList.remove('open');
306    }
307    
308    const popupBg = document.querySelector('.popup__bg');
309    
310    if (popupBg) {
311        popupBg.classList.remove('open');
312    }
313};
314
315// Обработчик отправки форм
316document.addEventListener('wpcf7mailsent', event => {
317    const formId = event.detail.contactFormId;
318    
319    // Ищем форму по ID через скрытое поле _wpcf7
320    let form = null;
321    const formInput = document.querySelector(`input[name="_wpcf7"][value="${formId}"]`);
322    
323    if (formInput) {
324        form = formInput.closest('form');
325    } else {
326        // Если не нашли через _wpcf7, ищем форму по unit_tag из события
327        const unitTag = event.detail.unitTag;
328        if (unitTag) {
329            // Извлекаем ID из unitTag (формат: wpcf7-f{ID}-p{postID}-o{order})
330            const match = unitTag.match(/wpcf7-f(\d+)/);
331            if (match && match[1] == formId) {
332                form = document.querySelector(`form[data-wpcf7-id="${formId}"], form.wpcf7-form`);
333            }
334        }
335        
336        // Если все еще не нашли, ищем любую форму с классом wpcf7-form на странице
337        if (!form) {
338            const allForms = document.querySelectorAll('form.wpcf7-form');
339            // Проверяем каждую форму на наличие нужного ID
340            allForms.forEach(f => {
341                const hiddenInput = f.querySelector(`input[name="_wpcf7"][value="${formId}"]`);
342                if (hiddenInput) {
343                    form = f;
344                }
345            });
346        }
347    }
348    
349    if (!form) {
350        console.warn('Form not found for ID:', formId);
351        return;
352    }
353    
354    const container = form.closest('.container');
355
356    if (form.closest('.additional-form.is--active')) return;
357
358    // Скрытие сообщений об ошибках
359    document.querySelectorAll('.wpcf7-response-output').forEach(output => {
360        output.style.display = 'none';
361    });
362
363    // редирект на страницу "Спасибо" при ее наличии
364    if( form.querySelector('input[name="redirect_page"]')) {
365        window.location.href = form.querySelector('input[name="redirect_page"]').value;
366        return false;
367    }
368    const isSubscriptionForm = ['13726'].includes(formId.toString()) || 
369                               form.closest('.subscription') !== null ||
370                               form.closest('.section.subscription') !== null;
371    
372    const formTypes = {
373        onboarding: form.querySelector('input[name=onboarding_form], input[name=content_form]'),
374        special: isSubscriptionForm,
375        form_new: form.querySelector('input[name=form_new_form]'),
376        authors: form.querySelector('input[name=authors_form]')
377    };
378
379    const popupConfigs = {
380        onboarding: { 
381            main: '.onboarding-popup', 
382            sent: '#popup-sent',
383            bg: '.popup__bg'
384        },
385        special: { 
386            main: '.free-form-popup', 
387            sent: '#subscribe',
388            bg: '.popup__bg'
389        },
390        form_new: { 
391            main: '.form-new-popup', 
392            sent: '#form-new-popup-sent',
393            bg: null
394        },
395        authors: { 
396            main: '.authors-popup', 
397            sent: '#authors-popup-sent',
398            bg: null
399        }
400    };
401
402    // Обработка типа формы
403    for (const [type, condition] of Object.entries(formTypes)) {
404        if (condition) {
405            const config = popupConfigs[type];
406            
407            // Закрытие основного попапа (только если он открыт)
408            if (config.main) {
409                const mainPopup = document.querySelector(config.main);
410                if (mainPopup && mainPopup.classList.contains('open')) {
411                    mainPopup.classList.remove('open');
412                }
413            }
414            
415            // Открытие попапа отправки
416            const sentPopup = document.querySelector(config.sent);
417            
418            if (sentPopup) {
419                // Получаем сообщение из ответа сервера (проверяем разные варианты)
420                const serverMessage = event.detail?.apiResponse?.message || 
421                                    event.detail?.message || 
422                                    (event.detail?.apiResponse && typeof event.detail.apiResponse === 'string' ? JSON.parse(event.detail.apiResponse)?.message : null);
423                
424                // Если есть сообщение от сервера и это форма подписки с сообщением о повторной подписке
425                if (serverMessage && isSubscriptionForm && serverMessage.includes('уже подписаны')) {
426                    // Используем специальный попап для уже подписанных
427                    const alreadySubscribedPopup = document.querySelector('#subscribe-already');
428                    
429                    if (alreadySubscribedPopup) {
430                        // Закрываем стандартный попап
431                        sentPopup.classList.remove('open');
432                        
433                        // Открываем попап для уже подписанных
434                        if (config.bg) {
435                            const bg = document.querySelector(config.bg);
436                            if (bg) {
437                                bg.classList.add('open');
438                            }
439                        }
440                        
441                        setTimeout(() => {
442                            alreadySubscribedPopup.classList.add('open');
443                            document.body.classList.add('is--hidden');
444                        }, 50);
445                        
446                        return;
447                    }
448                }
449                
450                // Открытие фона сначала (если есть)
451                if (config.bg) {
452                    const bg = document.querySelector(config.bg);
453                    if (bg) {
454                        bg.classList.add('open');
455                    }
456                }
457                
458                // Небольшая задержка для плавного открытия
459                setTimeout(() => {
460                    sentPopup.classList.add('open');
461                    // Блокируем скролл body при открытии попапа
462                    document.body.classList.add('is--hidden');
463                }, 50);
464            } else {
465                console.warn('Sent popup not found:', config.sent, 'for form ID:', formId, 'type:', type);
466            }
467            
468            return;
469        }
470    }
471});
472
473// Добавление UTM-меток
474(() => {
475    const allowedUTMKeys = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_content', 'utm_term'];
476    const utmParams = Object.fromEntries(
477        new URLSearchParams(window.location.search).entries()
478    );
479    
480    const filteredUTM = Object.fromEntries(
481        Object.entries(utmParams).filter(([key]) => allowedUTMKeys.includes(key))
482    );
483    
484    if (Object.keys(filteredUTM).length === 0) return;
485
486    document.querySelectorAll('form').forEach(form => {
487        Object.entries(filteredUTM).forEach(([key, value]) => {
488            if (!form.querySelector(`[name="${key}"]`)) {
489                const input = document.createElement('input');
490                input.type = 'hidden';
491                input.name = key;
492                input.value = value;
493                form.appendChild(input);
494            }
495        });
496    });
497})();
498
499// Кросс-доменная подписка через REST-endpoint WP (работает и на живом WP, и на статике
500// на другом домене). Endpoint + публичный site-key reCAPTCHA приходят из wp_localize_script
501// (window.dolphinSubscribe). Секрет reCAPTCHA — только на сервере.
502async function getRecaptchaToken() {
503    const cfg = window.dolphinSubscribe || {};
504    if (!cfg.recaptchaSiteKey || typeof grecaptcha === 'undefined' || !grecaptcha.execute) {
505        return '';
506    }
507    try {
508        return await new Promise((resolve) => {
509            grecaptcha.ready(() => {
510                grecaptcha.execute(cfg.recaptchaSiteKey, { action: 'subscribe' })
511                    .then(resolve)
512                    .catch(() => resolve(''));
513            });
514        });
515    } catch (e) {
516        return '';
517    }
518}
519
520async function dolphinSubscribeRequest(email, consent, honeypot) {
521    const cfg = window.dolphinSubscribe || {};
522    if (!cfg.endpoint) {
523        return { success: false, message: 'Подписка временно недоступна' };
524    }
525
526    const token = await getRecaptchaToken();
527
528    let res;
529    try {
530        res = await fetch(cfg.endpoint, {
531            method: 'POST',
532            headers: { 'Content-Type': 'application/json' },
533            body: JSON.stringify({
534                email: email,
535                consent: !!consent,
536                recaptcha_token: token,
537                website: honeypot || '' // honeypot
538            })
539        });
540    } catch (e) {
541        return { success: false, message: 'Произошла ошибка. Попробуйте позже.' };
542    }
543
544    // REST отдаёт { success, message } верхним уровнем (не вложено в data).
545    let data = {};
546    try { data = await res.json(); } catch (e) { /* пустой/невалидный ответ */ }
547
548    return {
549        success: !!data.success,
550        message: data.message || (res.ok ? 'Готово' : 'Произошла ошибка. Попробуйте позже.')
551    };
552}
553
554// Honeypot-поле, если его нет в разметке формы (для статики/динамических форм).
555function ensureHoneypot(form) {
556    let hp = form.querySelector('input[name="website"]');
557    if (!hp) {
558        hp = document.createElement('input');
559        hp.type = 'text';
560        hp.name = 'website';
561        hp.tabIndex = -1;
562        hp.autocomplete = 'off';
563        hp.setAttribute('aria-hidden', 'true');
564        hp.style.cssText = 'position:absolute;left:-9999px;width:1px;height:1px;opacity:0;';
565        form.appendChild(hp);
566    }
567    return hp;
568}
569
570function initHeroNewsletterSubscribeForms() {
571    const subscribeForms = document.querySelectorAll('.hero-new__subscribe-form');
572
573    subscribeForms.forEach(function(form) {
574        if (form.dataset.subscribeInit === '1') return;
575        form.dataset.subscribeInit = '1';
576        ensureHoneypot(form);
577
578        form.addEventListener('submit', function(e) {
579            e.preventDefault();
580            e.stopPropagation();
581
582            const formElement = e.target;
583            const emailInput = formElement.querySelector('.hero-new__subscribe-input');
584            const consentCheckbox = formElement.querySelector('#privacy-consent');
585            const submitButton = formElement.querySelector('button[type="submit"]');
586            
587            if (!emailInput || !consentCheckbox) {
588                return;
589            }
590            
591            const email = emailInput.value.trim();
592            const consent = consentCheckbox.checked;
593            
594            // Валидация
595            if (!email) {
596                alert('Пожалуйста, введите email адрес');
597                emailInput.focus();
598                return;
599            }
600            
601            // Простая валидация email
602            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
603            if (!emailRegex.test(email)) {
604                alert('Пожалуйста, введите корректный email адрес');
605                emailInput.focus();
606                return;
607            }
608            
609            if (!consent) {
610                alert('Необходимо дать согласие на обработку персональных данных');
611                consentCheckbox.focus();
612                return;
613            }
614            
615            if (!submitButton) return;
616
617            const originalText = submitButton.textContent;
618            submitButton.disabled = true;
619            submitButton.textContent = 'Отправка...';
620
621            const honeypot = ensureHoneypot(formElement).value;
622
623            dolphinSubscribeRequest(email, consent, honeypot)
624                .then(function(result) {
625                    alert(result.message);
626                    if (result.success) {
627                        const m = result.message.toLowerCase();
628                        const alreadyInList = m.includes('уже был добавлен')
629                            || m.includes('уже подписаны')
630                            || m.includes('уже в списке');
631                        if (!alreadyInList) {
632                            emailInput.value = '';
633                            consentCheckbox.checked = false;
634                        }
635                    } else {
636                        console.error('Dolphin Subscribers: Ошибка от сервера:', result);
637                    }
638                })
639                .finally(function() {
640                    submitButton.disabled = false;
641                    submitButton.textContent = originalText;
642                });
643        });
644    });
645}
646
647function initSubscribeBannerForms() {
648    const forms = document.querySelectorAll('.subscribe-banner__form');
649
650    const setState = (form, state) => {
651        const banner = form.closest('.subscribe-banner');
652
653        if (banner) {
654            banner.classList.remove('subscribe-banner--error');
655            if (state === 'error') banner.classList.add('subscribe-banner--error');
656        }
657    };
658
659    const validateEmail = (email) => {
660        const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
661        return emailRegex.test(email);
662    };
663
664    forms.forEach((form) => {
665        if (form.dataset.subscribeInit === '1') return;
666        form.dataset.subscribeInit = '1';
667        ensureHoneypot(form);
668
669        const emailInput = form.querySelector('.subscribe-banner__input');
670        if (emailInput) {
671            emailInput.addEventListener('input', () => {
672                setState(form, null);
673                emailInput.removeAttribute('aria-invalid');
674            });
675        }
676
677        form.addEventListener('submit', function(e) {
678            e.preventDefault();
679            e.stopPropagation();
680
681            const submitButton = form.querySelector('button[type="submit"]');
682
683            if (!emailInput || !submitButton) return;
684
685            const email = emailInput.value.trim();
686
687            setState(form, null);
688
689            if (!email) {
690                setState(form, 'error');
691                emailInput.setAttribute('aria-invalid', 'true');
692                emailInput.focus();
693                return;
694            }
695
696            if (!validateEmail(email)) {
697                setState(form, 'error');
698                emailInput.setAttribute('aria-invalid', 'true');
699                emailInput.focus();
700                return;
701            }
702
703            const originalText = submitButton.textContent;
704            submitButton.disabled = true;
705            submitButton.textContent = 'Отправка...';
706
707            // У subscribe-banner чекбокса нет — согласие по факту отправки формы.
708            const honeypot = ensureHoneypot(form).value;
709
710            dolphinSubscribeRequest(email, true, honeypot)
711                .then(function(result) {
712                    if (result.success) {
713                        const m = result.message.toLowerCase();
714                        const alreadyInList = m.includes('уже был добавлен')
715                            || m.includes('уже подписаны')
716                            || m.includes('уже в списке');
717                        if (!alreadyInList) {
718                            emailInput.value = '';
719                        }
720                    } else {
721                        setState(form, 'error');
722                        console.error('Dolphin Subscribers: Ошибка от сервера:', result);
723                    }
724                    alert(result.message);
725                })
726                .finally(function() {
727                    submitButton.disabled = false;
728                    submitButton.textContent = originalText;
729                });
730        });
731    });
732}
733
734function initNewsletterForms() {
735    initHeroNewsletterSubscribeForms();
736    initSubscribeBannerForms();
737}
738
739if (document.readyState === 'loading') {
740    document.addEventListener('DOMContentLoaded', initNewsletterForms);
741} else {
742    initNewsletterForms();
743}
744
745// Отмечаем, что скрипт загружен
746window.formJsLoaded = true;

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.