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)}®ion_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.