https://www.commune-nointel.fr/wp-content/plugins/hob-formchat-pro/assets/js/hob-address.js?ver=1.29
1/** 2 * HOB FormChat Pro â Address Autocomplete Module 3 * 4 * Supports 3 providers: 5 * - BAN (Base Adresse Nationale â French government API) 6 * - Google Places 7 * - Nominatim / Photon (OpenStreetMap fallback) 8 * 9 * Config is read from window.hobFcAddress, localized by WordPress. 10 * 11 * @package HOB_FormChat_Pro 12 * @since 1.0.0 13 * @license GPLv2+ 14 * @copyright 2026 HOB France 15 */ 16 17'use strict'; 18(function () { 19 20 var CFG = window.hobFcAddress || {}; 21 22 /* ------------------------------------------------------------------ */ 23 /* Utilities */ 24 /* ------------------------------------------------------------------ */ 25 26 /** 27 * Basic HTML entity escaping. 28 * 29 * @param {string} str 30 * @return {string} 31 */ 32 function escHtml(str) { 33 if (!str) return ''; 34 return String(str) 35 .replace(/&/g, '&') 36 .replace(/</g, '<') 37 .replace(/>/g, '>') 38 .replace(/"/g, '"'); 39 } 40 41 /** 42 * Simple debounce helper. 43 * 44 * @param {Function} fn 45 * @param {number} delay 46 * @return {Function} 47 */ 48 function debounce(fn, delay) { 49 var timer = null; 50 return function () { 51 var ctx = this; 52 var args = arguments; 53 if (timer) clearTimeout(timer); 54 timer = setTimeout(function () { 55 fn.apply(ctx, args); 56 }, delay); 57 }; 58 } 59 60 /* ------------------------------------------------------------------ */ 61 /* 1. resolveProvider */ 62 /* ------------------------------------------------------------------ */ 63 64 /** 65 * Determine which provider to use. 66 * 67 * Priority: explicit forceProvider > auto detection. 68 * Auto mode: BAN if France scope, Google if key configured, else Nominatim. 69 * France scope = country includes "fr" or is empty. 70 * 71 * @param {string} fieldCountry Country restriction from the field (e.g. "fr"). 72 * @param {string} forceProvider Explicit provider override. 73 * @return {string} "ban"|"google"|"nominatim" 74 */ 75 function resolveProvider(fieldCountry, forceProvider) { 76 if (forceProvider && forceProvider !== 'auto') { 77 return forceProvider; 78 } 79 80 var country = (fieldCountry || '').toLowerCase().trim(); 81 var isFranceScope = !country || country === 'fr'; 82 83 if (isFranceScope) { 84 return 'ban'; 85 } 86 87 if (CFG.googleKey) { 88 return 'google'; 89 } 90 91 return 'nominatim'; 92 } 93 94 /* ------------------------------------------------------------------ */ 95 /* 11. hideSuggestions */ 96 /* ------------------------------------------------------------------ */ 97 98 /** 99 * Hide the suggestion list and clear ARIA attributes. 100 * 101 * @param {HTMLElement} sugList 102 */ 103 function hideSuggestions(sugList) { 104 sugList.classList.remove('visible'); 105 var input = sugList.parentNode 106 ? sugList.parentNode.querySelector('.hob-fc-address-input') 107 : null; 108 if (input) { 109 input.removeAttribute('aria-expanded'); 110 } 111 } 112 113 /* ------------------------------------------------------------------ */ 114 /* 9. initKeyboardNav */ 115 /* ------------------------------------------------------------------ */ 116 117 /** 118 * Keyboard navigation inside the suggestion list. 119 * 120 * @param {HTMLInputElement} input 121 * @param {HTMLElement} sugList 122 */ 123 function initKeyboardNav(input, sugList) { 124 var currentIdx = -1; 125 126 input.addEventListener('keydown', function (e) { 127 var items = sugList.querySelectorAll('li:not(.hob-fc-address-suggestion--disabled):not(.hob-fc-address-suggestion--footer)'); 128 if (!items.length) return; 129 130 if (e.key === 'ArrowDown' || e.keyCode === 40) { 131 e.preventDefault(); 132 currentIdx = (currentIdx + 1 < items.length) ? currentIdx + 1 : 0;
133 setActive(items, currentIdx); 134 } else if (e.key === 'ArrowUp' || e.keyCode === 38) { 135 e.preventDefault(); 136 currentIdx = (currentIdx - 1 >= 0) ? currentIdx - 1 : items.length - 1; 137 setActive(items, currentIdx); 138 } else if (e.key === 'Enter' || e.keyCode === 13) { 139 e.preventDefault(); 140 if (currentIdx >= 0 && currentIdx < items.length) { 141 items[currentIdx].click(); 142 } 143 currentIdx = -1; 144 } else if (e.key === 'Escape' || e.keyCode === 27) { 145 e.preventDefault(); 146 hideSuggestions(sugList); 147 currentIdx = -1; 148 } 149 }); 150 151 function setActive(items, idx) { 152 for (var i = 0; i < items.length; i++) { 153 items[i].removeAttribute('aria-selected'); 154 items[i].classList.remove('hob-fc-address-suggestion--active'); 155 } 156 if (items[idx]) { 157 items[idx].setAttribute('aria-selected', 'true'); 158 items[idx].classList.add('hob-fc-address-suggestion--active'); 159 items[idx].scrollIntoView({ block: 'nearest' }); 160 } 161 } 162 } 163 164 /* ------------------------------------------------------------------ */ 165 /* 10. initClickOutside */ 166 /* ------------------------------------------------------------------ */ 167 168 /** 169 * Close suggestion list when clicking outside the wrapper. 170 * 171 * @param {HTMLInputElement} input 172 * @param {HTMLElement} sugList 173 */ 174 function initClickOutside(input, sugList) { 175 document.addEventListener('click', function (e) { 176 var wrapper = sugList.closest('.hob-fc-address-wrapper'); 177 if (wrapper && !wrapper.contains(e.target)) { 178 hideSuggestions(sugList); 179 } 180 }); 181 } 182 183 /* ------------------------------------------------------------------ */ 184 /* 8. fillSubfields */ 185 /* ------------------------------------------------------------------ */ 186 187 /** 188 * Fill decomposed sub-fields from a parts object. 189 * 190 * Looks for inputs with id = fieldId + '_' + key, or name containing the key. 191 * 192 * @param {HTMLElement} wrapper 193 * @param {string} fieldId 194 * @param {Object} parts 195 */ 196 function fillSubfields(wrapper, fieldId, parts) { 197 var keys = [ 198 'street_number', 'street', 'zip', 'city', 199 'state', 'country', 'lat', 'lng', 200 'citycode', 'ban_id' 201 ]; 202 203 keys.forEach(function (key) { 204 if (typeof parts[key] === 'undefined') return; 205 206 var value = parts[key] || ''; 207 208 // Try id-based lookup first 209 var el = wrapper.querySelector('#' + CSS.escape(fieldId + '_' + key)); 210 211 // Fallback: any input whose name contains the key 212 if (!el) { 213 el = wrapper.querySelector('input[name*="' + key + '"]'); 214 } 215 216 if (el) { 217 el.value = value; 218 // Trigger change for any framework listeners 219 var evt = new Event('change', { bubbles: true }); 220 el.dispatchEvent(evt); 221 } 222 }); 223 } 224 225 /* ------------------------------------------------------------------ */ 226 /* 4. renderBanSuggestions */ 227 /* ------------------------------------------------------------------ */ 228 229 /** 230 * Render BAN suggestion items into the list. 231 * 232 * @param {HTMLElement} sugList 233 * @param {Array} features GeoJSON features from BAN API 234 * @param {Function} onSelect Callback receiving a feature object 235 */ 236 function renderBanSuggestions(sugList, features, onSelect) { 237 sugList.innerHTML = ''; 238 239 if (!features || !features.length) { 240 var empty = document.createElement('li'); 241 empty.className = 'hob-fc-address-suggestion hob-fc-address-suggestion--disabled'; 242 empty.setAttribute('role', 'option'); 243 empty.setAttribute('aria-disabled', 'true'); 244 empty.textContent = 'Aucune adresse trouv\u00e9e'; 245 sugList.appendChild(empty); 246 sugList.classList.add('visible'); 247 return; 248 } 249 250 var typeIcons = { 251 housenumber: '\uD83C\uDFE0', // house 252 street: '\uD83D\uDEE3\uFE0F', // road 253 locality: '\uD83C\uDFD8\uFE0F', // houses 254 municipality:'\uD83C\uDFD9\uFE0F' // city 255 }; 256
257 features.forEach(function (feat, idx) { 258 var p = feat.properties || {}; 259 var li = document.createElement('li'); 260 li.className = 'hob-fc-address-suggestion'; 261 li.setAttribute('role', 'option'); 262 li.setAttribute('id', 'hob-fc-sug-' + idx); 263 264 var icon = typeIcons[p.type] || '\uD83D\uDCCD'; // pin fallback 265 var mainText = escHtml(p.name || p.label || ''); 266 var contextParts = (p.context || '').split(','); 267 var department = contextParts.length > 1 ? contextParts[1].trim() : ''; 268 var secondary = [p.postcode, p.city, department].filter(Boolean).join(' \u2014 '); 269 270 li.innerHTML = 271 '<span class="hob-fc-address-suggestion__icon">' + icon + '</span>' + 272 '<span class="hob-fc-address-suggestion__content">' + 273 '<span class="hob-fc-address-suggestion__main">' + mainText + '</span>' + 274 '<span class="hob-fc-address-suggestion__secondary">' + escHtml(secondary) + '</span>' + 275 '</span>' + 276 '<span class="hob-fc-address-suggestion__badge ban">BAN</span>'; 277 278 li.addEventListener('click', function () { 279 onSelect(feat); 280 }); 281 282 li.addEventListener('mouseenter', function () { 283 var all = sugList.querySelectorAll('.hob-fc-address-suggestion'); 284 for (var i = 0; i < all.length; i++) { 285 all[i].removeAttribute('aria-selected'); 286 } 287 li.setAttribute('aria-selected', 'true'); 288 }); 289 290 sugList.appendChild(li); 291 }); 292 293 // Powered-by footer 294 var footer = document.createElement('li'); 295 footer.className = 'hob-fc-address-suggestion hob-fc-address-suggestion--footer'; 296 footer.setAttribute('aria-disabled', 'true'); 297 footer.innerHTML = 298 'Donn\u00e9es \u00a9 <a href="https://adresse.data.gouv.fr" target="_blank" rel="noopener">Base Adresse Nationale</a>' + 299 ' \u2014 Etalab / La Poste / IGN'; 300 sugList.appendChild(footer); 301 302 sugList.classList.add('visible'); 303 var input = sugList.parentNode 304 ? sugList.parentNode.querySelector('.hob-fc-address-input') 305 : null; 306 if (input) { 307 input.setAttribute('aria-expanded', 'true'); 308 } 309 } 310 311 /* ------------------------------------------------------------------ */ 312 /* 5. selectBanSuggestion */ 313 /* ------------------------------------------------------------------ */ 314 315 /** 316 * Handle selection of a BAN suggestion. 317 * 318 * @param {Object} feat GeoJSON feature 319 * @param {HTMLInputElement} input 320 * @param {HTMLElement} sugList 321 * @param {HTMLInputElement} fullInput 322 * @param {HTMLElement} wrapper 323 * @param {string} fieldId 324 */ 325 function selectBanSuggestion(feat, input, sugList, fullInput, wrapper, fieldId) { 326 var p = feat.properties || {}; 327 var coords = (feat.geometry && feat.geometry.coordinates) ? feat.geometry.coordinates : [null, null]; 328 var lng = coords[0]; 329 var lat = coords[1]; 330 331 var contextParts = (p.context || '').split(','); 332 var state = contextParts.length > 1 ? contextParts[1].trim() : ''; 333 334 var parts = { 335 street_number: p.housenumber || '', 336 street: p.street || p.name || '', 337 zip: p.postcode || '', 338 city: p.city || '', 339 citycode: p.citycode || '', 340 state: state, 341 country: 'France', 342 lat: lat != null ? String(lat) : '', 343 lng: lng != null ? String(lng) : '', 344 ban_id: p.id || '', 345 full: p.label || '' 346 }; 347 348 input.value = parts.full; 349 if (fullInput) { 350 fullInput.value = parts.full; 351 } 352 353 fillSubfields(wrapper, fieldId, parts); 354 355 // Set hidden citycode and ban_id fields (outside wrapper too) 356 var form = wrapper.closest('form'); 357 if (form) { 358 var citycodeEl = form.querySelector('input[name*="citycode"]'); 359 if (citycodeEl) citycodeEl.value = parts.citycode; 360 var banIdEl = form.querySelector('input[name*="ban_id"]'); 361 if (banIdEl) banIdEl.value = parts.ban_id; 362 } 363 364 hideSuggestions(sugList); 365 } 366 367 /* ------------------------------------------------------------------ */ 368 /* 3. initBanAutocomplete */ 369 /* ------------------------------------------------------------------ */ 370 371 /** 372 * Initialise BAN (Base Adresse Nationale) autocomplete on a field. 373 * 374 * @param {HTMLElement} wrapper 375 * @param {HTMLInputElement} input 376 * @param {HTMLElement} sugList 377 * @param {HTMLElement} spinner 378 * @param {HTMLInputElement} fullInput 379 * @param {string} fieldId 380 * @param {string} country 381 * @param {string} mode 382 */ 383 function initBanAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode) { 384 var BAN_URL = CFG.banUrl || 'https://api-adresse.data.gouv.fr/search/'; 385 var abortCtrl = null; 386 387 var onInput = debounce(function () { 388 var query = input.value.trim(); 389 390 if (query.length < (CFG.minChars || 3)) { 391 hideSuggestions(sugList); 392 return; 393 } 394 395 // Build parameters 396 var params = new URLSearchParams(); 397 params.set('q', query); 398 params.set('limit', '6'); 399 params.set('autocomplete', '1'); 400 401 // Detect 5-digit postcode 402 var pcMatch = query.match(/\b(\d{5})\b/);
403 if (pcMatch) { 404 params.set('postcode', pcMatch[1]); 405 } 406 407 // Show spinner 408 spinner.classList.add('visible'); 409 410 // Abort previous request 411 if (abortCtrl) { 412 try { abortCtrl.abort(); } catch (ignore) {} 413 } 414 abortCtrl = typeof AbortController !== 'undefined' ? new AbortController() : null; 415 416 var fetchOpts = {}; 417 if (abortCtrl) { 418 fetchOpts.signal = abortCtrl.signal; 419 } 420 421 fetch(BAN_URL + '?' + params.toString(), fetchOpts) 422 .then(function (res) { return res.json(); }) 423 .then(function (data) { 424 spinner.classList.remove('visible'); 425 var features = data.features || []; 426 renderBanSuggestions(sugList, features, function (feat) { 427 selectBanSuggestion(feat, input, sugList, fullInput, wrapper, fieldId); 428 }); 429 }) 430 .catch(function (err) { 431 spinner.classList.remove('visible'); 432 if (err && err.name === 'AbortError') return; 433 console.warn('[HOB Address] BAN request failed, falling back to Nominatim:', err); 434 // Silent fallback to Nominatim 435 initNominatimAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode); 436 }); 437 }, CFG.debounceMs || 280); 438 439 input.addEventListener('input', onInput); 440 441 initKeyboardNav(input, sugList); 442 initClickOutside(input, sugList); 443 } 444 445 /* ------------------------------------------------------------------ */ 446 /* 6. initNominatimAutocomplete */ 447 /* ------------------------------------------------------------------ */ 448 449 /** 450 * Initialise Nominatim / Photon autocomplete on a field. 451 * 452 * @param {HTMLElement} wrapper 453 * @param {HTMLInputElement} input 454 * @param {HTMLElement} sugList 455 * @param {HTMLElement} spinner 456 * @param {HTMLInputElement} fullInput 457 * @param {string} fieldId 458 * @param {string} country 459 * @param {string} mode 460 */ 461 function initNominatimAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode) { 462 var PHOTON_URL = CFG.photonUrl || 'https://photon.komoot.io/api/'; 463 var abortCtrl = null; 464 465 // Remove any previous BAN listener by replacing the input 466 var fresh = input.cloneNode(true); 467 input.parentNode.replaceChild(fresh, input); 468 input = fresh; 469 470 var onInput = debounce(function () { 471 var query = input.value.trim(); 472 473 if (query.length < (CFG.minChars || 3)) { 474 hideSuggestions(sugList); 475 return; 476 } 477 478 var params = new URLSearchParams(); 479 params.set('q', query); 480 params.set('limit', '5'); 481 params.set('lang', 'fr'); 482 483 // Country bias 484 if (country) { 485 params.set('osm_tag', 'place'); 486 // Photon doesn't have a direct country param but we can use bbox or lang 487 // We pass the country hint via the "location_bias" workaround if France 488 // For a simple implementation, we include country name in the query 489 } 490 491 spinner.classList.add('visible'); 492 493 if (abortCtrl) { 494 try { abortCtrl.abort(); } catch (ignore) {} 495 } 496 abortCtrl = typeof AbortController !== 'undefined' ? new AbortController() : null; 497 498 var fetchOpts = {}; 499 if (abortCtrl) { 500 fetchOpts.signal = abortCtrl.signal; 501 } 502 503 fetch(PHOTON_URL + '?' + params.toString(), fetchOpts) 504 .then(function (res) { return res.json(); }) 505 .then(function (data) { 506 spinner.classList.remove('visible'); 507 var features = data.features || []; 508 renderNominatimSuggestions(sugList, features, function (feat) { 509 selectNominatimSuggestion(feat, input, sugList, fullInput, wrapper, fieldId); 510 }); 511 }) 512 .catch(function (err) { 513 spinner.classList.remove('visible'); 514 if (err && err.name === 'AbortError') return; 515 console.warn('[HOB Address] Photon/Nominatim request failed:', err); 516 }); 517 }, CFG.debounceMs || 280); 518 519 input.addEventListener('input', onInput); 520 521 initKeyboardNav(input, sugList); 522 initClickOutside(input, sugList); 523 } 524 525 /**
526 * Render Nominatim/Photon suggestion items. 527 * 528 * @param {HTMLElement} sugList 529 * @param {Array} features 530 * @param {Function} onSelect 531 */ 532 function renderNominatimSuggestions(sugList, features, onSelect) { 533 sugList.innerHTML = ''; 534 535 if (!features || !features.length) { 536 var empty = document.createElement('li'); 537 empty.className = 'hob-fc-address-suggestion hob-fc-address-suggestion--disabled'; 538 empty.setAttribute('role', 'option'); 539 empty.setAttribute('aria-disabled', 'true'); 540 empty.textContent = 'Aucune adresse trouv\u00e9e'; 541 sugList.appendChild(empty); 542 sugList.classList.add('visible'); 543 return; 544 } 545 546 features.forEach(function (feat, idx) { 547 var p = feat.properties || {}; 548 var li = document.createElement('li'); 549 li.className = 'hob-fc-address-suggestion'; 550 li.setAttribute('role', 'option'); 551 li.setAttribute('id', 'hob-fc-sug-nom-' + idx); 552 553 var mainText = escHtml(p.name || ''); 554 var secondaryParts = [p.postcode, p.city, p.state, p.country].filter(Boolean); 555 var secondary = secondaryParts.join(' \u2014 '); 556 557 li.innerHTML = 558 '<span class="hob-fc-address-suggestion__icon">\uD83C\uDF0D</span>' + 559 '<span class="hob-fc-address-suggestion__content">' + 560 '<span class="hob-fc-address-suggestion__main">' + mainText + '</span>' + 561 '<span class="hob-fc-address-suggestion__secondary">' + escHtml(secondary) + '</span>' + 562 '</span>' + 563 '<span class="hob-fc-address-suggestion__badge osm">OSM</span>'; 564 565 li.addEventListener('click', function () { 566 onSelect(feat); 567 }); 568 569 li.addEventListener('mouseenter', function () { 570 var all = sugList.querySelectorAll('.hob-fc-address-suggestion'); 571 for (var i = 0; i < all.length; i++) { 572 all[i].removeAttribute('aria-selected'); 573 } 574 li.setAttribute('aria-selected', 'true'); 575 }); 576 577 sugList.appendChild(li); 578 }); 579 580 // Powered-by footer 581 var footer = document.createElement('li'); 582 footer.className = 'hob-fc-address-suggestion hob-fc-address-suggestion--footer'; 583 footer.setAttribute('aria-disabled', 'true'); 584 footer.innerHTML = 'Donn\u00e9es \u00a9 <a href="https://www.openstreetmap.org/copyright" target="_blank" rel="noopener">OpenStreetMap</a> contributors'; 585 sugList.appendChild(footer); 586 587 sugList.classList.add('visible'); 588 var input = sugList.parentNode 589 ? sugList.parentNode.querySelector('.hob-fc-address-input') 590 : null; 591 if (input) { 592 input.setAttribute('aria-expanded', 'true'); 593 } 594 } 595 596 /** 597 * Handle selection of a Nominatim/Photon suggestion. 598 * 599 * @param {Object} feat 600 * @param {HTMLInputElement} input 601 * @param {HTMLElement} sugList 602 * @param {HTMLInputElement} fullInput 603 * @param {HTMLElement} wrapper 604 * @param {string} fieldId 605 */ 606 function selectNominatimSuggestion(feat, input, sugList, fullInput, wrapper, fieldId) { 607 var p = feat.properties || {}; 608 var coords = (feat.geometry && feat.geometry.coordinates) ? feat.geometry.coordinates : [null, null]; 609 var lng = coords[0]; 610 var lat = coords[1]; 611 612 var streetParts = []; 613 if (p.housenumber) streetParts.push(p.housenumber); 614 if (p.street) streetParts.push(p.street); 615 var fullAddress = [p.name || streetParts.join(' '), p.postcode, p.city, p.country].filter(Boolean).join(', '); 616 617 var parts = { 618 street_number: p.housenumber || '', 619 street: p.street || p.name || '', 620 zip: p.postcode || '', 621 city: p.city || '', 622 state: p.state || '', 623 country: p.country || '', 624 lat: lat != null ? String(lat) : '', 625 lng: lng != null ? String(lng) : '', 626 full: fullAddress 627 }; 628 629 input.value = parts.full; 630 if (fullInput) { 631 fullInput.value = parts.full; 632 } 633 634 fillSubfields(wrapper, fieldId, parts); 635 hideSuggestions(sugList); 636 } 637 638 /* ------------------------------------------------------------------ */ 639 /* 7. initGoogleAutocomplete */ 640 /* ------------------------------------------------------------------ */ 641 642 /** 643 * Initialise Google Places autocomplete on a field. 644 * 645 * @param {HTMLElement} wrapper 646 * @param {HTMLInputElement} input 647 * @param {HTMLElement} sugList 648 * @param {HTMLElement} spinner 649 * @param {HTMLInputElement} fullInput 650 * @param {string} fieldId 651 * @param {string} country 652 * @param {string} mode 653 */ 654 function initGoogleAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode) { 655 if (typeof google === 'undefined' || !google.maps || !google.maps.places) {
656 console.warn('[HOB Address] Google Maps Places API not loaded. Falling back to Nominatim.'); 657 initNominatimAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode); 658 return; 659 } 660 661 var options = { 662 types: ['address'], 663 fields: ['address_components', 'formatted_address', 'geometry'] 664 }; 665 666 if (country) { 667 options.componentRestrictions = { country: country }; 668 } 669 670 var autocomplete = new google.maps.places.Autocomplete(input, options); 671 672 autocomplete.addListener('place_changed', function () { 673 var place = autocomplete.getPlace(); 674 if (!place || !place.address_components) return; 675 676 var components = place.address_components; 677 var parts = { 678 street_number: '', 679 street: '', 680 zip: '', 681 city: '', 682 state: '', 683 country: '', 684 lat: '', 685 lng: '', 686 full: place.formatted_address || '' 687 }; 688 689 // Map Google component types to our parts keys 690 var typeMap = { 691 'street_number': 'street_number', 692 'route': 'street', 693 'postal_code': 'zip', 694 'locality': 'city', 695 'administrative_area_level_1': 'state', 696 'country': 'country' 697 }; 698 699 components.forEach(function (comp) { 700 comp.types.forEach(function (type) { 701 if (typeMap[type]) { 702 parts[typeMap[type]] = (type === 'country') 703 ? comp.long_name 704 : comp.long_name; 705 } 706 }); 707 }); 708 709 // Geometry 710 if (place.geometry && place.geometry.location) { 711 parts.lat = String(place.geometry.location.lat()); 712 parts.lng = String(place.geometry.location.lng()); 713 } 714 715 input.value = parts.full; 716 if (fullInput) { 717 fullInput.value = parts.full; 718 } 719 720 fillSubfields(wrapper, fieldId, parts); 721 }); 722 } 723 724 /* ------------------------------------------------------------------ */ 725 /* 2. initAddressField */ 726 /* ------------------------------------------------------------------ */ 727 728 /** 729 * Initialise a single address field wrapper. 730 * 731 * @param {HTMLElement} wrapper A .hob-fc-address-wrapper element. 732 */ 733 function initAddressField(wrapper) { 734 var fieldId = wrapper.getAttribute('data-field-id') || ''; 735 var mode = wrapper.getAttribute('data-mode') || 'autocomplete_decomposed'; 736 var country = wrapper.getAttribute('data-country') || CFG.defaultCountry || ''; 737 var forceProvider = wrapper.getAttribute('data-force-provider') || ''; 738 739 // Main input 740 var input = wrapper.querySelector('.hob-fc-address-input'); 741 if (!input) return; 742 743 // Suggestion list â get or create 744 var sugList = wrapper.querySelector('.hob-fc-address-suggestions'); 745 if (!sugList) { 746 sugList = document.createElement('ul'); 747 sugList.className = 'hob-fc-address-suggestions'; 748 sugList.setAttribute('role', 'listbox'); 749 wrapper.appendChild(sugList); 750 } 751 752 // Spinner â get or create 753 var spinner = wrapper.querySelector('.hob-fc-address-spinner'); 754 if (!spinner) { 755 spinner = document.createElement('span'); 756 spinner.className = 'hob-fc-address-spinner'; 757 wrapper.appendChild(spinner); 758 } 759 760 // Hidden full-address input 761 var fullInput = wrapper.querySelector('.hob-fc-address-full'); 762 763 // ARIA 764 input.setAttribute('role', 'combobox'); 765 input.setAttribute('aria-autocomplete', 'list'); 766 input.setAttribute('aria-controls', sugList.id || ''); 767 input.setAttribute('autocomplete', 'off'); 768 769 // Resolve provider and dispatch 770 var provider = resolveProvider(country, forceProvider); 771 772 switch (provider) { 773 case 'ban': 774 initBanAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode); 775 break; 776 case 'google': 777 initGoogleAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode); 778 break; 779 case 'nominatim': 780 default: 781 initNominatimAutocomplete(wrapper, input, sugList, spinner, fullInput, fieldId, country, mode); 782 break; 783 } 784 } 785 786 /* ------------------------------------------------------------------ */ 787 /* Bootstrap */ 788 /* ------------------------------------------------------------------ */ 789 790 document.addEventListener('DOMContentLoaded', function () { 791 document.querySelectorAll('.hob-fc-address-wrapper').forEach(initAddressField); 792 });
793 794})();
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.