PageSourceSearch

https://beeline-ru.online/wp-content/themes/beeline/js/cf7-address-search.js?ver=1789366998

js beeline-ru.online collected 2026-10-08 07:18:03 UTC 23,541 bytes, 577 lines download raw bytes

1/**
2 * Refactored Address Autocomplete Script
3 * VERSION 5: Adds immediate validation for the house field.
4 */
5(function () {
6    const STREET_MIN_LENGTH = 3;
7    const REQUEST_DELAY = 150;
8    const CACHE_LIMIT = 100;
9    const streetCache = new Map();
10    const houseCache = new Map();
11
12    // Run the script only after the entire page has loaded.
13    document.addEventListener('DOMContentLoaded', () => {
14
15        const popupSelectCity = document.getElementById('popup-select-city');
16        if (popupSelectCity) {
17            popupSelectCity.addEventListener('click', handleGlobalCityChange);
18        }
19
20        document.querySelectorAll('form').forEach(form => {
21            if (form.querySelector('[name="AddresStreet"]') && form.querySelector('[name="AddresHouse"]')) {
22                initializeFormAddressAutocomplete(form);
23            }
24        });
25    });
26
27    /**
28     * Updates all forms when a city is chosen from the global popup.
29     */
30    function handleGlobalCityChange(e) {
31        if (!e.target.classList.contains('region_item')) return;
32
33        const popup = e.currentTarget;
34
35        // LANDINGI-796: если попап открыт из бизнес-попапа, выбором города уже
36        // занялась отдельная ветка в popup-select-city.js (пишет только в форму
37        // внутри .popup-business). Не дублируем работу и не трогаем остальные
38        // формы страницы в этом случае.
39        if (popup.dataset.openedFrom === 'business') return;
40
41        const cityName = e.target.textContent.trim();
42        const regionId = e.target.getAttribute('id');
43
44        document.querySelectorAll('form').forEach(form => {
45            const cityDisplay = form.querySelector('.autocomplete-city-name');
46            if (cityDisplay) {
47                cityDisplay.textContent = cityName;
48                cityDisplay.setAttribute('data-item', regionId);
49                resetAddressFields(form);
50            }
51        });
52
53        popup.style.display = 'none';
54    }
55
56    /**
57     * Sets up all logic and event listeners for a single form's address fields.
58     * @param {HTMLFormElement} form - The form element to initialize.
59     */
60    function initializeFormAddressAutocomplete(form) {
61        if (form.dataset.addressAutocompleteInitialized) return;
62
63        const elements = {
64            cityDisplay: form.querySelector('.autocomplete-city-name'),
65            streetInput: form.querySelector('[name="AddresStreet"]'),
66            houseInput: form.querySelector('[name="AddresHouse"]'),
67            streetList: form.querySelector('#street-list'),
68            houseList: form.querySelector('#house-list'),
69            addressDisplay: form.querySelector('#address-block__title'),
70            hiddenCityId: form.querySelector('.id_city'),
71            hiddenCityName: form.querySelector('.name_city'),
72            hiddenStreetId: form.querySelector('.id_street'),
73            hiddenDistrictId: form.querySelector('.district'),
74            hiddenHouseId: form.querySelector('.house'),
75        };
76
77        if (!elements.cityDisplay || !elements.streetInput || !elements.houseInput || !elements.streetList || !elements.houseList) {
78            return;
79        }
80
81        const state = {
82            streetDebounceTimer: null,
83            houseDebounceTimer: null,
84            streetController: null,
85            houseController: null,
86            streetRequestSequence: 0,
87            houseRequestSequence: 0,
88            isHouseInvalid: true,
89        };
90        
91        elements.houseInput.disabled = true;
92        setupClearButtons(form);
93
94        elements.streetInput.addEventListener('input', () => {
95            cancelPendingRequest(state, 'street');
96            cancelPendingRequest(state, 'house');
97            clearTimeout(state.streetDebounceTimer);
98            resetHouseField(elements, state);
99            updateAddressDisplay(elements);
100            const query = elements.streetInput.value.trim();
101            const regionId = elements.cityDisplay.getAttribute('data-item') || cf7proxy.regionId;
102
103            elements.streetList.classList.add('hidden');
104
105            if (query.length < STREET_MIN_LENGTH) {
106                return;
107            }
108
109            const cacheKey = getStreetCacheKey(query, regionId);
110
111            if (streetCache.has(cacheKey)) {
112                renderStreetList(streetCache.get(cacheKey), elements);
113                return;
114            }
115
116            debounce(() => fetchStreets(query, elements, state), state, 'street');
117        });
118
119        elements.houseInput.addEventListener('input', () => {
120            cancelPendingRequest(state, 'house');
121            clearTimeout(state.houseDebounceTimer);
122            const query = elements.houseInput.value.trim();
123            const streetId = elements.houseInput.dataset.streetId;
124
125            elements.houseList.classList.add('hidden');
126
127            if (query.length < 1 || !streetId) {
128                return;
129            }
130
131            const cacheKey = getHouseCacheKey(query, streetId);
132
133            if (houseCache.has(cacheKey)) {
134                state.isHouseInvalid = true;
135                renderHouseList(houseCache.get(cacheKey), elements, state);
136                return;
137            }
138
139            debounce(() => fetchHouses(query, streetId, elements, state), state, 'house');
140        });
141        
142        form.addEventListener('keydown', (e) => handleEnterKey(e, elements, state));
143        document.addEventListener('click', (e) => handleDocumentClick(e, elements, state));
144        form.dataset.addressAutocompleteInitialized = 'true';
145    }
146    
147    // --- API & Rendering Functions ---
148
149    function fetchStreets(query, elements, state) {
150        const regionId = elements.cityDisplay.getAttribute('data-item') || cf7proxy.regionId;
151        const ajaxUrl = cf7proxy.ajaxUrl || '/wp-admin/admin-ajax.php';
152        const url = `${ajaxUrl}?action=cf7_proxy_streets&query=${encodeURIComponent(query)}&region_id=${encodeURIComponent(regionId)}`;
153        const requestSequence = ++state.streetRequestSequence;
154        const controller = createRequestController();
155        const cacheKey = getStreetCacheKey(query, regionId);
156
157        state.streetController = controller;
158
159        fetch(url, createFetchOptions(controller, 'streets'))
160            .then(res =>
160 res.ok ? res.json() : Promise.reject(res))
161            .then(response => {
162                const currentRegionId = elements.cityDisplay.getAttribute('data-item') || cf7proxy.regionId;
163
164                if (requestSequence !== state.streetRequestSequence ||
165                    elements.streetInput.value.trim() !== query ||
166                    String(currentRegionId) !== String(regionId)) {
167                    return;
168                }
169
170                const items = response?.data ?? [];
171                setAutocompleteCache(streetCache, cacheKey, items);
172                renderStreetList(items, elements);
173            })
174            .catch(err => {
175                if (err?.name === 'AbortError' || requestSequence !== state.streetRequestSequence) {
176                    return;
177                }
178
179                console.error('Street fetch error:', err);
180                elements.streetList.classList.add('hidden');
181            })
182            .finally(() => {
183                if (state.streetController === controller) {
184                    state.streetController = null;
185                }
186            });
187    }
188
189    function renderStreetList(items, elements) {
190        elements.streetList.innerHTML = '';
191        if (items.length === 0) {
192            elements.streetList.classList.add('hidden');
193            return;
194        }
195
196        items.forEach(item => {
197            const div = document.createElement('div');
198            div.className = 'autocomplete-item';
199            
200            const subtitleText = item.data?.subtitle?.trim() || item.subtitle?.trim() || '';
201            div.innerHTML = `
202                <div class="autocomplete-street">${item.highlight || item.data?.full || item.full || '—'}</div>
203                <div class="autocomplete-city">${subtitleText}</div>
204            `;
205            
206            div.addEventListener('click', () => {
207                if (subtitleText) {
208                    elements.cityDisplay.textContent = subtitleText;
209                    if (elements.hiddenCityName) elements.hiddenCityName.value = subtitleText;
210                }
211                
212                const newRegionId = item.data?.region_id;
213                if (newRegionId) {
214                    elements.cityDisplay.setAttribute('data-item', newRegionId);
215                    if (elements.hiddenCityId) elements.hiddenCityId.value = newRegionId;
216                }
217
218                elements.streetInput.value = item.data?.full || item.full || '';
219                
220                if (elements.hiddenStreetId) elements.hiddenStreetId.value = item.data?.street_id || '';
221                if (elements.hiddenDistrictId) elements.hiddenDistrictId.value = item.data?.district_id || '';
222                
223                elements.houseInput.disabled = false;
224                elements.houseInput.removeAttribute('readonly');
225                elements.houseInput.dataset.streetId = item.data?.street_id || '';
226                elements.houseInput.focus();
227                
228                updateAddressDisplay(elements);
229                elements.streetList.classList.add('hidden');
230            });
231
232            elements.streetList.appendChild(div);
233        });
234        elements.streetList.classList.remove('hidden');
235    }
236
237    function fetchHouses(query, streetId, elements, state) {
238        const ajaxUrl = cf7proxy.ajaxUrl || '/wp-admin/admin-ajax.php';
239        const url = `${ajaxUrl}?action=cf7_proxy_houses&street_id=${encodeURIComponent(streetId)}&query=${encodeURIComponent(query)}`;
240        const requestSequence = ++state.houseRequestSequence;
241        const controller = createRequestController();
242        const cacheKey = getHouseCacheKey(query, streetId);
243
244        state.houseController = controller;
245
246        fetch(url, createFetchOptions(controller, 'houses'))
247            .then(res => res.ok ? res.json() : Promise.reject(res))
248            .then(response => {
249                if (requestSequence !== state.houseRequestSequence ||
250                    elements.houseInput.value.trim() !== query ||
251                    String(elements.houseInput.dataset.streetId) !== String(streetId)) {
252                    return;
253                }
254
255                const items = response?.data ?? [];
256                setAutocompleteCache(houseCache, cacheKey, items);
257                state.isHouseInvalid = true;
258                renderHouseList(items, elements, state);
259            })
260            .catch(err => {
261                if (err?.name === 'AbortError' || requestSequence !== state.houseRequestSequence) {
262                    return;
263                }
264
265                console.error('House fetch error:', err);
266                elements.houseList.classList.add('hidden');
267            })
268            .finally(() => {
269                if (state.houseController === controller) {
270                    state.houseController = null;
271                }
272            });
273    }
274
275    function renderHouseList(items, elements, state) {
276        elements.houseList.innerHTML = '';
277
278        // ★★★ КЛЮЧЕВОЕ ИЗМЕНЕНИЕ ★★★
279        // Если API вернул пустой список (совпадений нет)...
280        if (items.length === 0) {
281            elements.houseList.classList.add('hidden');
282            // ...и пользователь что-то ввел в поле, то этот ввод невалиден.
283            if (elements.houseInput.value.trim() !== '') {
284                clearHouseInput(elements);
285                updateAddressDisplay(elements);
286            }
287            return;
288        }
289
290        items.forEach(item => {
291            const div = document.createElement('div');
292            div.className = 'autocomplete-item';
293            div.textContent = item.data?.full || item.full || '—';
294
295            if (elements.houseInput.value.trim().toLowerCase() === div.textContent.toLowerCase()) {
296                state.isHouseInvalid = false;
297                if (elements.hiddenHouseId) elements.hiddenHouseId.value = item.data?.id || '';
298            }
299
300            div.addEventListener('click', () => {
301                state.isHouseInvalid = false;
302                elements.houseInput.value = div.textContent;
303                if (elements.hiddenHouseId) elements.hiddenHouseId.value = item.data?.id || '';
304                updateAddressDisplay(elements);
305                elements.houseList.classList.add('hidden');
306                focusNextInput(elements.houseInput.form, elements.houseInput);
307            });
308            elements.houseList.appendChild(div);
309        });
310        elements.houseList.classList.remove('hidden');
311    }
312
313    // --- Event Handling Helpers ---
314
315    function handleEnterKey(e, elements, state) {
316        if (e.key !== 'Enter') return;
317        e.preventDefault();
318        const activeEl = document.activeElement;
319        const firstStreet = elements.streetList.querySelector('.autocomplete-item');
320        const firstHouse = elements.houseList.querySelector('.autocomplete-item');
321        if (activeEl === elements.streetInput && firstStreet) {
322            firstStreet.click();
323        } else if (activeEl === elements.houseInput) {
324            if (firstHouse) {
325                firstHouse.click();
326            } else {
327                if (state.isHouseInvalid) clearHouseInput(elements);
328                focusNextInput(e.target.form, activeEl);
329            }
330        } else {
331            focusNextInput(e.target.form, activeEl);
332        }
333    }
334
335    function handleDocumentClick(e, elements, state) {
336        if (!elements.streetList.contains(e.target) && !elements.streetInput.contains(e.target)) {
337            elements.streetList.classList.add('hidden');
338        }
339        if (!elements.houseList.contains(e.target) && !elements.houseInput.contains(e.target)) {
340            elements.houseList.classList.add('hidden');
341            if (state.isHouseInvalid && elements.houseInput.value.trim() !== '') {
342                clearHouseInput(elements);
343                updateAddressDisplay(elements);
344            }
345        }
346    }
347
348    // --- Utility & Helper Functions ---
349
350    function debounce(callback, state, type, delay = REQUEST_DELAY) {
351        const timerKey = `${type}DebounceTimer`;
352        clearTimeout(state[timerKey]);
353        state[timerKey] = setTimeout(callback, delay);
354    }
355
356    function cancelPendingRequest(state, type) {
357        const controllerKey = `${type}Controller`;
358        const sequenceKey = `${type}RequestSequence`;
359
360        state[sequenceKey] += 1;
361
362        if (state[controllerKey]) {
363            state[controllerKey].abort();
364            state[controllerKey] = null;
365        }
366    }
367
368    function createRequestController() {
369        return typeof AbortController === 'function' ? new AbortController() : null;
370    }
371
372    function createFetchOptions(controller, type) {
373        const options = {
374            credentials: 'omit',
375            headers: {
376                'X-Request-ID': createRequestId(type),
377            },
378            priority: 'high',
379        };
380
381        if (controller) {
382            options.signal = controller.signal;
383        }
384
385        return options;
386    }
387
388    function createRequestId(type) {
389        if (window.crypto && typeof window.crypto.randomUUID === 'function') {
390            return `${type}-${window.crypto.randomUUID()}`;
391        }
392
393        return `${type}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
394    }
395
396    function normalizeCacheValue(value) {
397        return String(value).trim().replace(/\s+/g, ' ').toLowerCase();
398    }
399
400    function getStreetCacheKey(query, regionId) {
401        return `${regionId}|${normalizeCacheValue(query)}`;
402    }
403
404    function getHouseCacheKey(query, streetId) {
405        return `${streetId}|${normalizeCacheValue(query)}`;
406    }
407
408    function setAutocompleteCache(cache, key, items) {
409        if (cache.size >= CACHE_LIMIT) {
410            cache.delete(cache.keys().next().value);
411        }
412
413        cache.set(key, items);
414    }
415
416    function updateAddressDisplay(elements) {
417        if (!elements.addressDisplay) return;
418        const parts = [
419            elements.cityDisplay.textContent.trim(),
420            elements.streetInput.value.trim(),
421            elements.houseInput.value.trim() ? `дом ${elements.houseInput.value.trim()}` : ''
422        ].filter(Boolean);
423        elements.addressDisplay.textContent = parts.length > 0 ? `Ваш адрес: ${parts.join(', ')}` : '';
424    }
425    
426    function resetAddressFields(form) {
427        const streetInput = form.querySelector('[name="AddresStreet"]');
428        const houseInput = form.querySelector('[name="AddresHouse"]');
429        const addressDisplay = form.querySelector('#address-block__title');
430        if (streetInput) streetInput.value = '';
431        if (houseInput) {
432            houseInput.value = '';
433            houseInput.disabled = true;
434            houseInput.placeholder = 'Дом';
435        }
436        ['.id_street', '.district', '.house'].forEach(selector => {
437            const hiddenInput = form.querySelector(selector);
438            if (hiddenInput) hiddenInput.value = '';
439        });
440        if (addressDisplay) addressDisplay.textContent = '';
441    }
442
443    function clearHouseInput(elements) {
444        elements.houseInput.value = '';
445        elements.houseInput.placeholder = 'Неверный дом';
446        if (elements.hiddenHouseId) elements.hiddenHouseId.value = '';
447    }
448
449    function resetHouseField(elements, state) {
450        elements.houseInput.value = '';
451        elements.houseInput.disabled = true;
452        elements.houseInput.placeholder = 'Дом';
453        if (elements.hiddenHouseId) elements.hiddenHouseId.value = '';
454        state.isHouseInvalid = true;
455    }
456    
457    function focusNextInput(form, currentInput) {
458        const inputs = Array.from(form.querySelectorAll('input:not([type="hidden"]), textarea, select, button'));
459        const currentIndex = inputs.indexOf(currentInput);
460        if (currentIndex > -1 && currentIndex < inputs.length - 1) {
461            inputs[currentIndex + 1].focus();
462        }
463    }
464    
465    function setupClearButtons(form) {
466        form.querySelectorAll('.clear-btn').forEach(btn => {
467            const targetInput = form.querySelector(`[name="${btn.dataset.target}"]`);
468            if (!targetInput) return;
469            const updateVisibility = () => btn.style.display = targetInput.value ? 'flex' : 'none';
470            targetInput.addEventListener('input', updateVisibility);
471            btn.addEventListener('click', () => {
472                targetInput.value = '';
473                targetInput.dispatchEvent(new Event('input', { bubbles: true }));
474                targetInput.focus();
475            });
476            updateVisibility();
477        });
478    }
479
480})();
481
482/**
483 * LANDINGI-796: начальное состояние города в попапе «Подключить услуги для бизнеса».
484 * Отдельный самодостаточный блок — с остальным содержимым файла не пересекается.
485 *
486 * При каждом открытии попапа город подставляется по приоритету:
487 *   1) ручной выбор в пределах этого открытия страницы (dataset на #popup-business);
488 *   2) город страницы (data-page-city-* из popup-business.php);
489 *   3) ранее сохранённый выбор (cookie business_popup_city, 7 дней);
490 *   4) «Выберите город» — тогда поле адреса заблокировано.
491 */
492(function () {
493    const BUSINESS_CITY_COOKIE = 'business_popup_city';
494    const EMPTY_CITY_TEXT = 'Выберите город';
495
496    function getBusinessCityCookie() {
497        const match = document.cookie.match(new RegExp('(^|;\\s*)' + BUSINESS_CITY_COOKIE + '=([^;]+)'));
498        if (!match) return null;
499        try {
500            const parsed = JSON.parse(decodeURIComponent(match.pop()));
501            return parsed && parsed.id && parsed.name ? parsed : null;
502        } catch (e) {
503            return null;
504        }
505    }
506
507    function isVisible(el) {
508        return !!el && window.getComputedStyle(el).display !== 'none';
509    }
510
511    function applyBusinessCityState(popup) {
512        const form = popup.querySelector('form.wpcf7-form');
513        if (!form) return;
514
515        const cityNameDisplay = form.querySelector('.autocomplete-city-name');
516        const hiddenCityId = form.querySelector('.id_city');     // BusinessCityId
517        const hiddenCityName = form.querySelector('.name_city'); // CityName
518        const addressInput = form.querySelector('[name="City"]');
519
520        let cityId = '';
521        let cityName = '';
522
523        if (popup.dataset.manualCityId && popup.dataset.manualCityName) {
524            // Ручной выбор в пределах этой загрузки страницы — важнее города страницы.
525            cityId = popup.dataset.manualCityId;
526            cityName = popup.dataset.manualCityName;
527        } else if (popup.dataset.pageCityId && popup.dataset.pageCityName) {
528            cityId = popup.dataset.pageCityId;
529            cityName = popup.dataset.pageCityName;
530        } else {
531            const saved = getBusinessCityCookie();
532            if (saved) {
533                cityId = saved.id;
534                cityName = saved.name;
535            }
536        }
537
538        if (cityId && cityName) {
539            if (cityNameDisplay) {
540                cityNameDisplay.textContent = cityName;
541                cityNameDisplay.setAttribute('data-item', cityId);
542            }
543            if (hiddenCityId) hiddenCityId.value = cityId;
544            if (hiddenCityName) hiddenCityName.value = cityName;
545            if (addressInput) addressInput.disabled = false;
546        } else {
547            if (cityNameDisplay) {
548                cityNameDisplay.textContent = EMPTY_CITY_TEXT;
549                cityNameDisplay.setAttribute('data-item', '');
550            }
551            if (hiddenCityId) hiddenCityId.value = '';
552            if (hiddenCityName) hiddenCityName.value = '';
553            if (addressInput) addressInput.disabled = true;
554        }
555    }
556
557    document.addEventListener('DOMContentLoaded', () => {
558        const popup = document.getElementById('popup-business');
559        if (!popup) return;
560
561        // Состояние на момент загрузки страницы (попап скрыт, но поля уже должны быть согласованы).
562        applyBusinessCityState(popup);
563
564        // Попап показывается сменой style.display из main.js — ловим это наблюдателем,
565        // чтобы не зависеть от того, каким из механизмов открытия сработала кнопка.
566        let wasVisible = isVisible(popup);
567        const observer = new MutationObserver(() => {
568            const visible = isVisible(popup);
569            if (visible && !wasVisible) {
570                applyBusinessCityState(popup);
571            }
572            wasVisible = visible;
573        });
574
575        observer.observe(popup, { attributes: true, attributeFilter: ['style', 'class'] });
576    });
577})();

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.