1import { getConfig } from './config.js'; 2import { isAddressValidationEnabled, pickBestGeocodeResult } from './address-validation.js'; 3import { isCityBasedDelivery, isZoneBasedDelivery, usesDeliveryAreas } from './delivery-zones.js'; 4import { fetchPlacesApiKey } from './site-api.js'; 5 6const ELBLAG_BOUNDS = { 7 north: 54.195, 8 south: 54.118, 9 east: 19.485, 10 west: 19.355 11}; 12 13let mapsInitPromise = null; 14 15export function emptyAddressParts() { 16 return { city: '', street: '', number: '', apartment: '' }; 17} 18 19function findAddressPartInput(root, part) { 20 const scope = root && root.querySelector ? root : document; 21 if (scope !== document) { 22 const scoped = scope.querySelector(`[data-address-part="${part}"]`); 23 if (scoped) return scoped; 24 } 25 const idMap = { 26 city: 'address-city', 27 street: 'address-street', 28 number: 'address-number', 29 apartment: 'address-apartment' 30 }; 31 return document.getElementById(idMap[part] || ''); 32} 33 34export function readAddressPartsFromForm(root = document) { 35 return { 36 city: findAddressPartInput(root, 'city')?.value?.trim() || '', 37 street: findAddressPartInput(root, 'street')?.value?.trim() || '', 38 number: findAddressPartInput(root, 'number')?.value?.trim() || '', 39 apartment: findAddressPartInput(root, 'apartment')?.value?.trim() || '' 40 }; 41} 42 43export function composeDeliveryAddress(parts = {}) { 44 const city = String(parts.city || '').trim(); 45 const street = String(parts.street || '').trim(); 46 const number = String(parts.number || '').trim(); 47 const apartment = String(parts.apartment || '').trim(); 48
49 const house = [number, apartment].filter(Boolean).join('/'); 50 const line = [street, house].filter(Boolean).join(' ').trim(); 51 return [line, city].filter(Boolean).join(', '); 52} 53 54export function areRequiredAddressPartsFilled(parts = {}) { 55 return !!( 56 String(parts.city || '').trim() 57 && String(parts.street || '').trim() 58 && String(parts.number || '').trim() 59 ); 60} 61 62export function fillAddressPartsForm(parts = {}, root = document) { 63 const cityEl = findAddressPartInput(root, 'city'); 64 const streetEl = findAddressPartInput(root, 'street'); 65 const numberEl = findAddressPartInput(root, 'number'); 66 const aptEl = findAddressPartInput(root, 'apartment'); 67 if (cityEl && parts.city != null) cityEl.value = parts.city; 68 if (streetEl && parts.street != null) streetEl.value = parts.street; 69 if (numberEl && parts.number != null) numberEl.value = parts.number; 70 if (aptEl && parts.apartment != null) aptEl.value = parts.apartment; 71} 72 73function findAddressGroup(input) { 74 return input?.closest?.('[data-address-group], #address-group, .input-group--address') || null; 75} 76 77function componentLongName(components, ...types) { 78 const match = (components || []).find(component => 79 types.some(type => (component.types || []).includes(type)) 80 ); 81 return match?.long_name || ''; 82} 83 84export function parseGoogleAddressParts(components = []) { 85 const street = componentLongName(components, 'route'); 86 let number = componentLongName(components, 'street_number'); 87 let apartment = componentLongName(components, 'subpremise'); 88 const city = componentLongName(components, 'locality', 'postal_town') 89 || componentLongName(components, 'administrative_area_level_2'); 90 91 if (number.includes('/')) { 92 const [house, apt] = number.split('/'); 93 number = house.trim(); 94 if (!apartment) apartment = (apt || '').trim(); 95 } 96 97 return { 98 city, 99 street, 100 number, 101 apartment 102 }; 103} 104 105export function splitStreetLine(text = '') { 106 const raw = String(text || '').trim(); 107 if (!raw) return emptyAddressParts(); 108 109 const match = raw.match(/^(.*?)[\s,]+(\d+[a-zA-Z]?(?:-\d+[a-zA-Z]?)?)(?:\s*[/\s]\s*(\d+[a-zA-Z]?))?$/); 110 if (!match) { 111 return { ...emptyAddressParts(), street: raw }; 112 } 113 114 return { 115 city: '', 116 street: match[1].trim(), 117 number: match[2].trim(), 118 apartment: (match[3] || '').trim() 119 }; 120} 121 122/** Parsuje pojedynczy string adresu (np. z profilu) do pól koszyka. */ 123export function parseDeliveryAddressString(text = '') { 124 const raw = String(text || '').trim(); 125 if (!raw) return emptyAddressParts(); 126 127 const chunks = raw.split(',').map((s) => s.trim()).filter(Boolean); 128 if (chunks.length >= 2) { 129 const city = chunks[chunks.length - 1]; 130 const streetLine = chunks.slice(0, -1).join(', '); 131 return { ...splitStreetLine(streetLine), city }; 132 } 133 return splitStreetLine(raw); 134} 135 136export function addressPartsLabel(parts = {}) { 137 const composed = composeDeliveryAddress(parts); 138 return composed || ''; 139} 140 141function parseCityFromSecondary(text = '') { 142 return String(text || '') 143 .replace(/\b\d{2}-\d{3}\b/g, '') 144 .replace(/\bPolska\b/gi, '') 145 .replace(/\bPoland\b/gi, '') 146 .replace(/,\s*,/g, ',') 147 .replace(/^[\s,]+|[\s,]+$/g, '') 148 .trim(); 149} 150 151function parsePredictionParts(prediction) { 152 const main = prediction?.structured_formatting?.main_text || ''; 153 const secondary = prediction?.structured_formatting?.secondary_text || ''; 154 const fromMain = splitStreetLine(main); 155 const city = parseCityFromSecondary(secondary); 156 157 if (fromMain.street || fromMain.number) { 158 return { ...fromMain, city: city || fromMain.city }; 159 } 160 161 const description = String(prediction?.description || '') 162 .replace(/,\s*Polska$/i, '') 163 .replace(/,\s*Poland$/i, '') 164 .trim(); 165 const [line = '', cityPart = ''] = description.split(',').map(part => part.trim()); 166 return { ...splitStreetLine(line), city: city || cityPart }; 167} 168 169async function resolvePlacesApiKey(config = {}) { 170 const fromConfig = String(config?.delivery?.googleMapsPlacesKey || '').trim(); 171 if (fromConfig) return fromConfig; 172 return fetchPlacesApiKey(); 173} 174 175function loadGoogleMapsPlaces(apiKey) { 176 if (window.google?.maps?.places) { 177 return Promise.resolve(); 178 } 179 180 if (mapsInitPromise) { 181 return mapsInitPromise; 182 } 183 184 mapsInitPromise = new Promise((resolve, reject) => { 185 const script = document.createElement('script');
186 script.src = `https://maps.googleapis.com/maps/api/js?key=${encodeURIComponent(apiKey)}&libraries=places&language=pl®ion=PL&loading=async`; 187 script.async = true; 188 script.defer = true; 189 script.onload = () => resolve(); 190 script.onerror = () => reject(new Error('Nie udaÅo siÄ zaÅadowaÄ Google Maps.')); 191 document.head.appendChild(script); 192 }); 193 194 return mapsInitPromise; 195} 196 197function extractLatLngValue(value) { 198 if (typeof value === 'function') return Number(value()); 199 return Number(value); 200} 201 202function mapGeocoderResult(result) { 203 const rawLocation = result.geometry?.location; 204 const lat = extractLatLngValue(rawLocation?.lat); 205 const lng = extractLatLngValue(rawLocation?.lng); 206 const location = Number.isFinite(lat) && Number.isFinite(lng) ? { lat, lng } : null; 207 208 return { 209 formatted_address: result.formatted_address, 210 partial_match: result.partial_match, 211 geometry: { 212 location_type: result.geometry?.location_type || '', 213 location 214 }, 215 address_components: (result.address_components || []).map(component => ({ 216 long_name: component.long_name, 217 short_name: component.short_name, 218 types: component.types 219 })) 220 }; 221} 222 223/** Jak geocode.php: zwraca trafienie z wspóÅrzÄdnymi; strefy/miasta decydujÄ o akceptacji. */ 224function evaluationFromDeliveryAreaHit(mapped) { 225 const best = mapped.find(item => item.geometry?.location) || mapped[0]; 226 if (!best) return { valid: false, reason: 'not_found' }; 227 228 return { 229 valid: true, 230 reason: null, 231 formattedAddress: best.formatted_address || '', 232 location: best.geometry?.location || null, 233 address_components: best.address_components || [], 234 partial_match: !!best.partial_match, 235 geometry: best.geometry || null 236 }; 237} 238 239export async function geocodeAddressClient(address, config = {}) { 240 const apiKey = await resolvePlacesApiKey(config); 241 if (!apiKey) { 242 return { valid: false, reason: 'not_configured' }; 243 } 244 245 await loadGoogleMapsPlaces(apiKey); 246 247 const geocoder = new window.google.maps.Geocoder(); 248 const query = String(address || '').trim(); 249 const hasElblag = query.includes('ElblÄ g') || query.includes('Elblag'); 250 const geocodeQuery = hasElblag 251 ? query 252 : (usesDeliveryAreas(config) ? `${query}, Polska` : `${query}, ElblÄ g, Polska`); 253 254 return new Promise(resolve => { 255 geocoder.geocode( 256 { 257 address: geocodeQuery, 258 componentRestrictions: { country: 'pl' } 259 }, 260 (results, status) => { 261 if (status !== 'OK' || !Array.isArray(results) || results.length === 0) { 262 resolve({ valid: false, reason: 'not_found' }); 263 return; 264 } 265 266 const mapped = results.map(mapGeocoderResult); 267 if (usesDeliveryAreas(config)) { 268 const areaEval = evaluationFromDeliveryAreaHit(mapped); 269 if (isZoneBasedDelivery(config) && !areaEval.location) { 270 resolve({ valid: false, reason: 'not_found' }); 271 return; 272 } 273 resolve(areaEval); 274 return; 275 } 276 277 resolve(pickBestGeocodeResult(mapped)); 278 } 279 ); 280 }); 281} 282 283function getElblagBounds() { 284 return new window.google.maps.LatLngBounds( 285 { lat: ELBLAG_BOUNDS.south, lng: ELBLAG_BOUNDS.west }, 286 { lat: ELBLAG_BOUNDS.north, lng: ELBLAG_BOUNDS.east } 287 ); 288} 289 290function ensureSuggestionsList(input) { 291 const group = findAddressGroup(input); 292 if (!group) return null; 293 294 // Na mobile lista żyje w <body> (position:fixed), żeby nie ginÄÅa w overflow koszyka. 295 let list = group.querySelector('.address-suggestions'); 296 if (!list && input.id) { 297 list = document.querySelector(`.address-suggestions[data-for="${CSS.escape(input.id)}"]`); 298 } 299 if (!list) { 300 list = document.createElement('ul'); 301 list.className = 'address-suggestions'; 302 list.hidden = true; 303 } 304 305 if (input.id) list.dataset.for = input.id; 306 307 if (isCoarsePointer()) { 308 if (list.parentElement !== document.body) document.body.appendChild(list); 309 } else if (list.parentElement !== group) { 310 group.appendChild(list); 311 } 312 313 return list; 314} 315 316function isCoarsePointer() { 317 try { 318 return window.matchMedia('(pointer: coarse)').matches; 319 } catch { 320 return 'ontouchstart' in window; 321 } 322} 323 324function clearFixedSuggestionStyles(list) { 325 if (!list) return; 326 list.classList.remove('address-suggestions--fixed'); 327 list.style.position = ''; 328 list.style.top = ''; 329 list.style.bottom = ''; 330 list.style.left = ''; 331 list.style.right = ''; 332 list.style.width = ''; 333 list.style.maxHeight = ''; 334 list.style.zIndex = ''; 335} 336 337/** Fixed portal nad klawiaturÄ â unika obcinania przez overflow i skakania viewportu. */ 338function positionFixedSuggestions(input, list) { 339 if (!input || !list || list.hidden) return; 340 341 const rect = input.getBoundingClientRect();
342 const vv = window.visualViewport; 343 const offsetTop = vv?.offsetTop ?? 0; 344 const vpH = vv?.height ?? window.innerHeight; 345 const vpW = vv?.width ?? window.innerWidth; 346 const gap = 4; 347 const sidePad = 8; 348 const spaceBelow = vpH - (rect.bottom - offsetTop); 349 const spaceAbove = rect.top - offsetTop; 350 const maxH = Math.min(280, Math.floor(vpH * 0.42)); 351 const left = Math.min(Math.max(sidePad, rect.left), vpW - sidePad - 120); 352 const width = Math.max(120, Math.min(rect.width, vpW - left - sidePad)); 353 354 list.classList.add('address-suggestions--fixed'); 355 list.style.position = 'fixed'; 356 list.style.left = `${left}px`; 357 list.style.width = `${width}px`; 358 list.style.right = 'auto'; 359 list.style.zIndex = '4000'; 360 361 const preferBelow = spaceBelow >= 120 || spaceBelow >= spaceAbove; 362 if (preferBelow) { 363 const h = Math.max(96, Math.min(maxH, spaceBelow - gap - 8)); 364 list.style.top = `${rect.bottom + gap}px`; 365 list.style.bottom = 'auto'; 366 list.style.maxHeight = `${h}px`; 367 } else { 368 const h = Math.max(96, Math.min(maxH, spaceAbove - gap - 8)); 369 list.style.top = `${Math.max(offsetTop + sidePad, rect.top - gap - h)}px`; 370 list.style.bottom = 'auto'; 371 list.style.maxHeight = `${h}px`; 372 } 373} 374 375function suppressNativeAutofill(input) { 376 if (!input) return; 377 // WÅasna lista â wyÅÄ cz natywnÄ autofill / systemowe podpowiedzi klawiatury. 378 // ânew-passwordâ skuteczniej niż âoffâ tÅumi natywnÄ listÄ w Chrome/iOS. 379 input.setAttribute('autocomplete', 'new-password'); 380 input.setAttribute('autocorrect', 'off'); 381 input.setAttribute('autocapitalize', 'words'); 382 input.setAttribute('spellcheck', 'false'); 383 input.setAttribute('inputmode', 'text'); 384 input.setAttribute('data-lpignore', 'true'); 385 input.setAttribute('data-1p-ignore', 'true'); 386 input.setAttribute('data-form-type', 'other'); 387} 388 389function bindAddressSuggestions(input) { 390 if (input.dataset.addressSuggestionsBound === 'true') return; 391 392 const group = findAddressGroup(input) || document; 393 const service = new window.google.maps.places.AutocompleteService(); 394 const bounds = getElblagBounds(); 395 const list = ensureSuggestionsList(input); 396 if (!list) return; 397 398 let debounceTimer = null; 399 let activeIndex = -1; 400 let currentPredictions = []; 401 let suppressSuggestionsUntil = 0; 402 let selecting = false; 403 const mobile = isCoarsePointer(); 404 405 list.setAttribute('role', 'listbox'); 406 list.setAttribute('aria-label', 'Podpowiedzi adresu'); 407 408 const syncListPosition = () => { 409 if (list.hidden) return; 410 if (mobile) positionFixedSuggestions(input, list); 411 }; 412 413 const hideSuggestions = () => { 414 list.hidden = true; 415 list.innerHTML = ''; 416 activeIndex = -1; 417 currentPredictions = []; 418 clearFixedSuggestionStyles(list); 419 }; 420 421 const finishSelectionFocus = (parts) => { 422 if (!mobile) return; 423 424 const numberEl = findAddressPartInput(group, 'number'); 425 const hasNumber = !!(parts?.number || numberEl?.value?.trim()); 426 // Zawsze blur ulicy â koÅczy walkÄ klawiatury z listÄ / scrollIntoView. 427 input.blur(); 428 if (!hasNumber && numberEl) { 429 window.setTimeout(() => { 430 numberEl.focus({ preventScroll: true }); 431 numberEl.scrollIntoView({ block: 'nearest', behavior: 'auto' }); 432 }, 60); 433 } 434 }; 435 436 const selectPrediction = (prediction) => { 437 if (!prediction || selecting) return; 438 selecting = true; 439 suppressSuggestionsUntil = Date.now() + 600; 440 clearTimeout(debounceTimer); 441 442 const parts = parsePredictionParts(prediction); 443 hideSuggestions(); 444 fillAddressPartsForm(parts, group); 445 446 // Synthetic input dla wyceny dostawy â bez ponownego otwierania listy. 447 input.dispatchEvent(new Event('input', { bubbles: true })); 448 findAddressPartInput(group, 'city')?.dispatchEvent(new Event('input', { bubbles: true })); 449 findAddressPartInput(group, 'number')?.dispatchEvent(new Event('input', { bubbles: true })); 450 451 finishSelectionFocus(parts); 452 selecting = false; 453 }; 454 455 const renderSuggestions = (predictions) => { 456 if (Date.now() < suppressSuggestionsUntil) return; 457 currentPredictions = predictions; 458 activeIndex = -1; 459 // div + role=option (nie <button>) â na mobile button przejmuje fokus i otwiera/zamyka klawiaturÄ. 460 list.innerHTML = predictions.map((prediction, index) => ` 461 <li role="presentation"> 462 <div 463 class="address-suggestions__item" 464 data-index="${index}" 465 role="option" 466 tabindex="-1" 467 aria-selected="false" 468 >${prediction.description}</div> 469 </li> 470 `).join(''); 471 list.hidden = false; 472 syncListPosition(); 473 }; 474 475 const requestSuggestions = () => { 476 if (Date.now() < suppressSuggestionsUntil || selecting) { 477 hideSuggestions(); 478 return; 479 } 480 481 const parts = readAddressPartsFromForm(group); 482 const query = [parts.street, parts.number, parts.city].filter(
482Boolean).join(' ').trim() 483 || input.value.trim(); 484 if (query.length < 2) { 485 hideSuggestions(); 486 return; 487 } 488 489 service.getPlacePredictions( 490 { 491 input: query, 492 componentRestrictions: { country: 'pl' }, 493 bounds, 494 types: ['address'] 495 }, 496 (predictions, status) => { 497 if (Date.now() < suppressSuggestionsUntil || selecting) { 498 hideSuggestions(); 499 return; 500 } 501 if ( 502 status !== window.google.maps.places.PlacesServiceStatus.OK 503 || !Array.isArray(predictions) 504 || predictions.length === 0 505 ) { 506 hideSuggestions(); 507 return; 508 } 509 510 renderSuggestions(predictions); 511 } 512 ); 513 }; 514 515 input.addEventListener('input', () => { 516 if (Date.now() < suppressSuggestionsUntil || selecting) { 517 clearTimeout(debounceTimer); 518 hideSuggestions(); 519 return; 520 } 521 clearTimeout(debounceTimer); 522 debounceTimer = setTimeout(requestSuggestions, 220); 523 }); 524 525 input.addEventListener('focus', () => { 526 if (mobile && !list.hidden) syncListPosition(); 527 }); 528 529 input.addEventListener('keydown', (event) => { 530 if (list.hidden || !currentPredictions.length) return; 531 532 if (event.key === 'ArrowDown') { 533 event.preventDefault(); 534 activeIndex = Math.min(activeIndex + 1, currentPredictions.length - 1); 535 } else if (event.key === 'ArrowUp') { 536 event.preventDefault(); 537 activeIndex = Math.max(activeIndex - 1, 0); 538 } else if (event.key === 'Enter' && activeIndex >= 0) { 539 event.preventDefault(); 540 selectPrediction(currentPredictions[activeIndex]); 541 return; 542 } else if (event.key === 'Escape') { 543 hideSuggestions(); 544 return; 545 } else { 546 return; 547 } 548 549 list.querySelectorAll('.address-suggestions__item').forEach((item, index) => { 550 const active = index === activeIndex; 551 item.classList.toggle('is-active', active); 552 item.setAttribute('aria-selected', active ? 'true' : 'false'); 553 }); 554 }); 555 556 // pointerdown + preventDefault: wybór bez blur inputu i bez fokusu na pozycji listy. 557 let lastPickAt = 0; 558 const onPickPointer = (event) => { 559 const item = event.target.closest('.address-suggestions__item'); 560 if (!item || !list.contains(item)) return; 561 // Nie blokuj scrolla listy (tylko primary tap / lewy przycisk). 562 if (event.pointerType === 'mouse' && event.button !== 0) return; 563 event.preventDefault(); 564 event.stopPropagation(); 565 if (Date.now() - lastPickAt < 400) return; 566 lastPickAt = Date.now(); 567 const index = Number(item.dataset.index); 568 selectPrediction(currentPredictions[index]); 569 }; 570 if (window.PointerEvent) { 571 list.addEventListener('pointerdown', onPickPointer); 572 } else { 573 list.addEventListener('mousedown', onPickPointer); 574 list.addEventListener('touchstart', onPickPointer, { passive: false }); 575 } 576 577 const onOutsidePointer = (event) => { 578 if (list.hidden) return; 579 if (input.contains(event.target) || list.contains(event.target)) return; 580 hideSuggestions(); 581 }; 582 document.addEventListener(window.PointerEvent ? 'pointerdown' : 'mousedown', onOutsidePointer); 583 584 if (mobile) { 585 const scrollRoot = input.closest('.cart-panel__body, .order-form, .account-panel, main') || null; 586 scrollRoot?.addEventListener('scroll', syncListPosition, { passive: true }); 587 window.addEventListener('scroll', syncListPosition, { passive: true }); 588 window.addEventListener('resize', syncListPosition); 589 window.visualViewport?.addEventListener('resize', syncListPosition); 590 window.visualViewport?.addEventListener('scroll', syncListPosition); 591 } 592 593 suppressNativeAutofill(input); 594 input.dataset.addressSuggestionsBound = 'true'; 595} 596 597function parseNominatimAddressParts(data = {}) { 598 const a = data.address || {}; 599 let number = String(a.house_number || '').trim(); 600 let apartment = String(a.unit || a.flat || '').trim(); 601 if (number.includes('/')) { 602 const [house, apt] = number.split('/'); 603 number = house.trim();
604 if (!apartment) apartment = (apt || '').trim(); 605 } 606 607 return { 608 street: String(a.road || a.pedestrian || a.residential || a.street || '').trim(), 609 number, 610 apartment, 611 city: String(a.city || a.town || a.village || a.municipality || a.suburb || '').trim() 612 }; 613} 614 615async function reverseGeocodeWithNominatim(lat, lng) { 616 const url = new URL('https://nominatim.openstreetmap.org/reverse'); 617 url.searchParams.set('format', 'jsonv2'); 618 url.searchParams.set('lat', String(lat)); 619 url.searchParams.set('lon', String(lng)); 620 url.searchParams.set('addressdetails', '1'); 621 url.searchParams.set('accept-language', 'pl'); 622 url.searchParams.set('zoom', '18'); 623 624 const response = await fetch(url.toString(), { 625 headers: { Accept: 'application/json' } 626 }); 627 if (!response.ok) { 628 throw new Error('Nie udaÅo siÄ odczytaÄ adresu z lokalizacji.'); 629 } 630 631 const data = await response.json(); 632 const parts = parseNominatimAddressParts(data); 633 const address = composeDeliveryAddress(parts) || data.display_name || ''; 634 if (!address) { 635 throw new Error('Nie znaleziono adresu dla tej lokalizacji.'); 636 } 637 638 return { address, parts }; 639} 640 641async function reverseGeocodeWithGoogle(lat, lng, config = {}) { 642 const apiKey = await resolvePlacesApiKey(config); 643 if (!apiKey) return null; 644 645 await loadGoogleMapsPlaces(apiKey); 646 const geocoder = new window.google.maps.Geocoder(); 647 648 return new Promise(resolve => { 649 geocoder.geocode({ location: { lat, lng } }, (results, status) => { 650 if (status !== 'OK' || !Array.isArray(results) || !results.length) { 651 resolve(null); 652 return; 653 } 654 const best = results[0]; 655 const parts = parseGoogleAddressParts(best.address_components || []); 656 const address = composeDeliveryAddress(parts) || best.formatted_address || null; 657 if (!address) { 658 resolve(null); 659 return; 660 } 661 resolve({ address, parts }); 662 }); 663 }); 664} 665 666export async function reverseGeocodeLocation(lat, lng, config = getConfig()) { 667 try { 668 const googleResult = await reverseGeocodeWithGoogle(lat, lng, config); 669 if (googleResult?.address) return googleResult; 670 } catch (err) { 671 console.warn('Google reverse geocode niedostÄpne:', err); 672 } 673 674 return reverseGeocodeWithNominatim(lat, lng); 675} 676 677export function getCurrentPosition() { 678 return new Promise((resolve, reject) => { 679 if (!navigator.geolocation) { 680 reject(new Error('Twoja przeglÄ darka nie obsÅuguje lokalizacji GPS.')); 681 return; 682 } 683 684 navigator.geolocation.getCurrentPosition( 685 position => resolve(position.coords), 686 error => { 687 if (error.code === error.PERMISSION_DENIED) { 688 reject(new Error('Brak zgody na lokalizacjÄ. WÅÄ cz GPS w przeglÄ darce.')); 689 return; 690 } 691 if (error.code === error.POSITION_UNAVAILABLE) { 692 reject(new Error('Nie udaÅo siÄ pobraÄ lokalizacji GPS.')); 693 return; 694 } 695 if (error.code === error.TIMEOUT) { 696 reject(new Error('Czas oczekiwania na lokalizacjÄ minÄ Å. Spróbuj ponownie.')); 697 return; 698 } 699 reject(new Error('Nie udaÅo siÄ pobraÄ lokalizacji.')); 700 }, 701 { 702 enableHighAccuracy: true, 703 timeout: 15000, 704 maximumAge: 0 705 } 706 ); 707 }); 708} 709 710export async function locateAddressFromGps(config = getConfig()) { 711 const coords = await getCurrentPosition(); 712 const located = await reverseGeocodeLocation(coords.latitude, coords.longitude, config); 713 const address = typeof located === 'string' ? located : located.address; 714 const parts = typeof located === 'string' 715 ? { ...splitStreetLine(located), city: '' } 716 : (located.parts || emptyAddressParts()); 717 718 return { 719 address, 720 parts, 721 lat: coords.latitude, 722 lng: coords.longitude, 723 accuracy: coords.accuracy 724 }; 725} 726 727export async function initAddressAutocomplete(config = getConfig(), streetInput = null) { 728 const input = streetInput || document.getElementById('address-street'); 729 const apiKey = await resolvePlacesApiKey(config); 730 if (!input || !apiKey || !isAddressValidationEnabled(config)) return false; 731 if (input.dataset.addressSuggestionsBound === 'true') return true; 732 733 try { 734 await loadGoogleMapsPlaces(apiKey); 735 bindAddressSuggestions(input); 736 return true; 737 } catch (err) { 738 console.warn('Google Places suggestions niedostÄpne:', err); 739 return false; 740 } 741} 742 743/** Podpowiedzi adresu w dowolnym bloku (np. panel konta). */ 744export async function initAddressAutocompleteInRoot(root, config = getConfig()) { 745 if (!root) return false; 746 const input = root.querySelector('[data-address-part="street"]'); 747 if (!input) return false; 748 return initAddressAutocomplete(config, input); 749} 750 751export async function refreshAddressAutocompleteHint(config = getConfig()) { 752 const hint = document.getElementById('address-hint'); 753 if (!hint) return; 754 755 if (!isAddressValidationEnabled(config)) { 756 if (usesDeliveryAreas(config)) { 757 hint.textContent = isCityBasedDelivery(config) 758 ? 'Tryb miejscowoÅci â wpisz miasto, ulicÄ i numer domu.' 759 : 'Strefy dostawy wymagajÄ wÅÄ czonej weryfikacji Google Maps.'; 760 } else { 761 hint.textContent = 'Weryfikacja Google Maps wyÅÄ czona â podaj miasto, ulicÄ i numer domu.'; 762 } 763 return; 764 } 765 766 const hasPlacesKey = !!(await resolvePlacesApiKey(config)); 767 if (usesDeliveryAreas(config)) { 768 hint.textContent = hasPlacesKey 769 ? 'UzupeÅnij pola adresu â po weryfikacji pokażemy odlegÅoÅÄ, strefÄ i koszt dostawy.' 770 : 'Podaj miasto, ulicÄ i numer â po weryfikacji pokażemy odlegÅoÅÄ, strefÄ i koszt dostawy.'; 771 return; 772 } 773 774 hint.textContent = hasPlacesKey 775 ? 'Zacznij pisaÄ ulicÄ â wybierz podpowiedź z listy. Numer lokalu jest opcjonalny.' 776 : 'Podaj miasto, ulicÄ i numer domu. Numer lokalu jest opcjonalny.'; 777}
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.