1/* ----------------- Start Document ----------------- */ 2(function($){ 3"use strict"; 4 5$(document).ready(function(){ 6 7 var infoBox_ratingType = 'star-rating'; 8 var url; 9 10 /** 11 * The Google Maps API is loaded with loading=async, so google.maps may 12 * not exist yet at DOM ready. Runs cb once the API is usable: right away 13 * if present, via the listeoOnGoogleMaps queue (inline shim printed 14 * before the API tag) when available, otherwise with a short poll for 15 * setups that load the API by other means. 16 */ 17 function whenGoogleMapsReady(cb) { 18 if (window.google && window.google.maps && window.google.maps.Map) { cb(); return; } 19 if (window.listeoOnGoogleMaps) { window.listeoOnGoogleMaps(cb); return; } 20 var tries = 0; 21 var timer = setInterval(function() { 22 if (window.google && window.google.maps && window.google.maps.Map) { 23 clearInterval(timer); 24 cb(); 25 } else if (++tries >= 100) { 26 clearInterval(timer); 27 } 28 }, 100); 29 } 30 31 /** 32 * Validates and normalizes latitude and longitude coordinates 33 * @param {*} lat - Latitude value to validate 34 * @param {*} lng - Longitude value to validate 35 * @returns {object|null} - Object with {lat, lng} as numbers if valid, null otherwise 36 */ 37 function isValidCoordinate(lat, lng) { 38 // Check if values are not null, undefined, empty string 39 if (lat == null || lng == null || lat === '' || lng === '') { 40 console.warn('Listeo Map: Invalid coordinates - null or empty', {lat: lat, lng: lng}); 41 return null; 42 } 43 44 // Convert to numbers - handle both string and numeric input 45 var latNum = typeof lat === 'number' ? lat : parseFloat(String(lat).trim().replace(',', '.')); 46 var lngNum = typeof lng === 'number' ? lng : parseFloat(String(lng).trim().replace(',', '.')); 47 48 // Check if conversion resulted in valid numbers 49 if (isNaN(latNum) || isNaN(lngNum)) { 50 console.warn('Listeo Map: Invalid coordinates - not numeric', {lat: lat, lng: lng, parsed: {latNum: latNum, lngNum: lngNum}}); 51 return null; 52 } 53 54 // Check if latitude is within valid range (-90 to 90) 55 if (latNum < -90 || latNum > 90) { 56 console.warn('Listeo Map: Invalid latitude - out of range (-90 to 90)', {lat: latNum}); 57 return null; 58 } 59 60 // Check if longitude is within valid range (-180 to 180) 61 if (lngNum < -180 || lngNum > 180) { 62 console.warn('Listeo Map: Invalid longitude - out of range (-180 to 180)', {lng: lngNum}); 63 return null; 64 } 65 66 // Check for suspicious "zero coordinates" that might indicate missing data 67 if (latNum === 0 && lngNum === 0) { 68 console.warn('Listeo Map: Suspicious coordinates - both zero (0,0)', {lat: lat, lng: lng}); 69 return null; 70 } 71 72 // Return normalized numeric coordinates 73 return { 74 lat: latNum, 75 lng: lngNum 76 }; 77 } 78 79 function getMarkers() { 80 var arrMarkers = []; 81 $('.listing-geo-data').each(function(index) { 82 var point_address; 83 if( $( this ).data('friendly-address') ){ 84 point_address = $( this ).data('friendly-address'); 85 } else { 86 point_address = $( this ).data('address'); 87 } 88 89 if( $( this ).is('a') ){ 90 url = $(this).find('a').attr('href'); 91 } else { 92 url = $(this).find('a').attr('href'); 93 } 94 95 if( $( this ).data('longitude') ) { 96 // badges can be in listing-list-small-badges-container, or listing-small-list-badges-container or listing-small-badges-container 97 // add a check for each of them and use first on that is found 98 var badges; 99 if( $( this ).find('.listing-list-small-badges-container').length > 0 ) { 100 badges = $( this ).find('.listing-list-small-badges-container').html(); 101 } else if( $( this ).find('.listing-small-list-badges-container').length > 0 ) { 102 badges = $( this ).find('.listing-small-list-badges-container').html(); 103 } else { 104 badges = $( this ).find('.listing-small-badges-container').html(); 105 } 106 if( !badges ) { 107 badges = ""; 108 } 109 110 var rating; 111 if( $( this ).find('.listing-rating-nl').length > 0 ) { 112 rating = $( this ).find('.listing-rating-nl').html(); 113 } else { 114 rating = $(this).data("rating"); 115 } 116 117 arrMarkers.push([ 118 locationData( 119 $(this).find("a").attr("href") 120 ? $(this).find("a").attr("href") 121 : $(this).attr("href"), 122 $(this).data("image"), 123 $(this).data("title"), 124 $(this).data("listing-type"), 125 $(this).data("classifieds-price"), 126 point_address, 127 rating, 128 $(this).data("reviews"), 129 badges 130 ), 131 $(this).data("longitude"), 132 $(this).data("latitude"), 133 1, 134 $(this).data("icon"), 135 ]); 136 } 137 }); 138 139 return arrMarkers; 140 } 141 142 function starsOutput(firstStar, secondStar, thirdStar, fourthStar, fifthStar) { 143 return(''+ 144 '<span class="'+firstStar+'"></span>'+ 145 '<span class="'+secondStar+'"></span>'+ 146 '<span class="'+thirdStar+'"></span>'+ 147 '<span class="'+fourthStar+'"></span>'+ 148 '<span class="'+fifthStar+'"></span>'); 149 } 150 151 function locationData(locationURL,locationImg,locationTitle,locationType, locationPrice, locationAddress, locationRating, locationRatingCounter, badges) { 152 153 var output; 154 var output_top; 155 var output_bottom; 156 output_top= ''+ 157 '<a href="'+ locationURL +'" class="leaflet-listing-img-container">'+ 158 '<div class="listing-small-badges-container">' + badges + '</div>' + 159 '<img src="'+locationImg+'" alt="">'+ 160 '<div class="leaflet-listing-item-content">'+ 161 '<h3>'+locationTitle+'</h3>'+ 162 '<span>'+locationAddress+'</span>'+ 163 '</div>'+ 164 165 166 '</a>'+ 167 168 '<div class="leaflet-listing-content">'+ 169 '<div class="listing-title">'; 170 // classifieds and any non-bookable listing type expose a static
171 // price via data-classifieds-price (empty for bookable types) 172 if(locationType == 'classifieds' || locationPrice) { 173 // .data() returns the attribute entity-decoded; re-encode 174 // before concatenating into popup HTML 175 var safeLocationPrice = locationPrice ? $('<div>').text(locationPrice).html() : ''; 176 output_bottom = '<div class="classifieds-price-infobox">'+safeLocationPrice+'</div>'+ 177 '</div>'+ 178 '</div>'; 179 } else { 180 181 // check if locationRating is a number and greater than 0 182 183 if (typeof locationRating === 'number' && locationRating > 0) { 184 185 186 // Rating Stars Output 187 188 var fiveStars = starsOutput('star','star','star','star','star'); 189 190 var fourHalfStars = starsOutput('star','star','star','star','star half'); 191 var fourStars = starsOutput('star','star','star','star','star empty'); 192 193 var threeHalfStars = starsOutput('star','star','star','star half','star empty'); 194 var threeStars = starsOutput('star','star','star','star empty','star empty'); 195 196 var twoHalfStars = starsOutput('star','star','star half','star empty','star empty'); 197 var twoStars = starsOutput('star','star','star empty','star empty','star empty'); 198 199 var oneHalfStar = starsOutput('star','star half','star empty','star empty','star empty'); 200 var oneStar = starsOutput('star','star empty','star empty','star empty','star empty'); 201 202 // Rules 203 var stars; 204 if (locationRating >= 4.75) { 205 stars = fiveStars; 206 } else if (locationRating >= 4.25) { 207 stars = fourHalfStars; 208 } else if (locationRating >= 3.75) { 209 stars = fourStars; 210 } else if (locationRating >= 3.25) { 211 stars = threeHalfStars; 212 } else if (locationRating >= 2.75) { 213 stars = threeStars; 214 } else if (locationRating >= 2.25) { 215 stars = twoHalfStars; 216 } else if (locationRating >= 1.75) { 217 stars = twoStars; 218 } else if (locationRating >= 1.25) { 219 stars = oneHalfStar; 220 } else if (locationRating < 1.25) { 221 stars = oneStar; 222 } 223 224 225 output_bottom = '<div class="'+infoBox_ratingType+'" data-rating="'+locationRating+'"><div class="rating-counter">('+locationRatingCounter+' '+listeo_core.maps_reviews_text+')</div>'+stars+'</div>'+ 226 '</div>'+ 227 '</div>'; 228 } 229 // check if it's html code 230 else if (typeof locationRating === 'string' && locationRating.trim() !== '') { 231 output_bottom = 232 '<div class="listing-rating-nl">' + 233 locationRating + 234 "</div>" + 235 "</div>" + 236 "</div>"; 237 } else { 238 output_bottom = '<div class="'+infoBox_ratingType+'"><span class="not-rated">'+listeo_core.maps_noreviews_text+'</span></div>'+ 239 '</div>'+ 240 '</div>'; 241 } 242 } 243 244 output = output_top+output_bottom; 245 246 247 return output; 248 } 249 250 window.L_DISABLE_3D = true 251 252 253 // console.log(locations); 254 var group; 255 var marker; 256 var locations; 257 var markerArray = []; 258 259 // User location marker and radius circle variables 260 var userLocationMarker = null; 261 var radiusCircle = null; 262 var userLocationLayer = L.layerGroup(); 263 var userLocationFirstShow = true; 264 265 266 var latlngStr = listeo_core.centerPoint.split(",",2); 267 var lat = parseFloat(latlngStr[0]); 268 var lng = parseFloat(latlngStr[1]); 269 var mapZoomAttr = $('div#map').attr('data-map-zoom'); 270 var mapScrollAttr = $('div#map').attr('data-map-scroll'); 271 272 var zoomLevel = parseInt(mapZoomAttr); 273 274 var markers; 275 if ( $('div#map').hasClass('split-map') && !("ontouchstart" in document.documentElement)) { 276 var mapOptions = { 277 center: [lat,lng], 278 zoom: zoomLevel, 279 zoomControl: false, 280 gestureHandling: false, 281 tap: false 282 } 283 } else { 284 var mapOptions = { 285 center: [lat,lng], 286 zoom: zoomLevel, 287 zoomControl: false, 288 gestureHandling: false, 289 tap: false 290 } 291 } 292 293 // MarkerCluster needs a finite maxZoom before the async Google layer is added. 294 if (listeo_core.map_provider === "google") { 295 mapOptions.maxZoom = 18; 296 } 297 298 299 var _map = document.querySelectorAll("div#map"); 300 // if querySelectorAll has found an element 301 302 303 304 if (_map.length > 0) { 305 window.map = L.map("map", mapOptions); 306 307 switch (listeo_core.map_provider) { 308 case "osm": 309 L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 310 attribution: 311 '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors', 312 }).addTo(map); 313 break; 314 315 case "google": 316 var googleMutantOptions = { 317 type: "roadmap", // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid' 318 maxZoom: 18, 319 }; 320 321 // Add Map ID if configured (for Cloud Styled Maps) 322 if (listeo_core.google_maps_id && listeo_core.google_maps_id !== '') { 323 googleMutantOptions.mapId = listeo_core.google_maps_id; 324 } 325 326 // Deferred so googleMutant's internal 10s wait-for-API timeout 327 // can't throw when the async API loads slowly. 328 whenGoogleMapsReady(function() { 329 L.gridLayer.googleMutant(googleMutantOptions).addTo(map); 330 }); 331 332 break; 333 334 case "mapbox": 335 var accessToken = listeo_core.mapbox_access_token; 336 var mapbox_style_url = listeo_core.mapbox_style_url; 337 338 if (listeo_core.mapbox_retina) { 339 L.tileLayer(mapbox_style_url + accessToken, { 340 attribution: 341 " © <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> © <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>", 342 maxZoom: 18, 343 zoomOffset: -1, 344 //detectRetina: true, 345 tileSize: 512, 346 }).addTo(map); 347 } else { 348 L.tileLayer(mapbox_style_url + accessToken, { 349 attribution: 350 " © <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> © <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>", 351 maxZoom: 18, 352 //detectRetina: true, 353 id: "mapbox.streets", 354 }).addTo(map); 355 } 356 357 break; 358 359 case "bing": 360 var options = { 361 bingMapsKey: listeo_core.bing_maps_key, 362 imagerySet: "RoadOnDemand", 363 }; 364 365 L.tileLayer.bing(options).addTo(map); 366 break; 367 368 case "thunderforest": 369 var tileUrl = 370 "https://tile.thunderforest.com/cycle/{z}/{x}/{y}{r}.png?apikey=" + 371 listeo_core.thunderforest_api_key, 372 layer = new L.TileLayer(tileUrl, { maxZoom: 18 }); 373 map.addLayer(layer); 374 break; 375 376 case "here": 377 L.tileLayer 378 .here({ 379 appId: listeo_core.here_app_id, 380 appCode: listeo_core.here_app_code, 381 }) 382 .addTo(map); 383 break; 384 } 385 386 // Check if scroll should be enabled based on data attribute 387 var scrollEnabled = false;
388 if (typeof mapScrollAttr !== typeof undefined && mapScrollAttr !== false) { 389 scrollEnabled = (mapScrollAttr === 'true' || mapScrollAttr === '1' || mapScrollAttr === 1); 390 } 391 392 // Enable or disable scroll based on the attribute 393 if (!scrollEnabled) { 394 map.scrollWheelZoom.disable(); 395 } 396 397 // Add zoom controls 398 var zoomOptions = { 399 zoomInText: '<i class="fa fa-plus" aria-hidden="true"></i>', 400 zoomOutText: '<i class="fa fa-minus" aria-hidden="true"></i>', 401 }; 402 var zoom = L.control.zoom(zoomOptions); 403 zoom.addTo(map); 404 405 // Modify where the map is initialized (around line 472) 406 if (window.map) { 407 // Add user location layer to map 408 window.map.addLayer(userLocationLayer); 409 410 // Add flag to track if map was moved by user 411 var userMovedMap = false; 412 var initialLoadComplete = false; 413 var ignoreMapEvents = false; 414 // Programmatic auto-location movement (geolocate button flyTo) is not 415 // a visitor map move and must not arm the map-bounds search. 416 var autoLocationMapMove = false; 417 var autoLocationFailsafeTimer = null; 418 419 // Set initial load complete after a short delay 420 setTimeout(function() { 421 initialLoadComplete = true; 422 }, 5000); 423 424 // Add movestart event to track user interaction 425 window.map.on("movestart", function(e) { 426 // Only set userMovedMap if initial load is complete and event is triggered by user action 427 if(initialLoadComplete && !!e.originalEvent) { 428 userMovedMap = true; 429 } 430 }); 431 432 // NEW: Add event listener for dragstart to detect user initiated map movement 433 window.map.on('dragstart', function(e) { 434 if(ignoreMapEvents || autoLocationMapMove) return; 435 userMovedMap = true; 436 }); 437 438 // NEW: Add event listener for zoomstart to detect user initiated zooming 439 // Leaflet also emits zoomstart for flyTo()/fitBounds(), hence the 440 // autoLocationMapMove guard. Leaflet 1.1.1 never passes originalEvent 441 // to dragstart/zoomstart, so the flag is the only reliable signal. 442 window.map.on('zoomstart', function(e) { 443 if(ignoreMapEvents || autoLocationMapMove) return; 444 userMovedMap = true; 445 }); 446 447 // Add zoomend event to detect when user has finished zooming 448 window.map.on('zoomend', function(e) { 449 if(ignoreMapEvents) return; 450 if(!!e.originalEvent) { 451 userMovedMap = true; 452 userLocationFirstShow = false; // User has interacted, no more auto-fitting 453 } 454 }); 455 456 // Add moveend event to detect when user has finished panning 457 window.map.on('moveend', function(e) { 458 if (autoLocationMapMove) { 459 // Consume the programmatic geolocate flyTo: clear the flags and 460 // any map-bounds inputs a stray earlier interaction injected. 461 clearTimeout(autoLocationFailsafeTimer); 462 userMovedMap = false; 463 autoLocationMapMove = false; 464 $('#listeo_core-search-form') 465 .find('.map-bounds, input[name="search_by_map_move"]') 466 .remove(); 467 setTimeout(function () { ignoreMapEvents = false; }, 100); 468 return; 469 } 470 if(ignoreMapEvents) return; 471 if(!!e.originalEvent) { 472 userMovedMap = true; 473 userLocationFirstShow = false; // User has interacted, no more auto-fitting 474 } 475 }); 476 477 // Add moveend event with debounce 478 window.map.on( 479 "moveend", 480 debounce(function () { 481 if (!initialLoadComplete) { 482 return; 483 } 484 485 if (userMovedMap && listeo_core.map_bounds_search == "on") { 486 updateListingsForMapBounds(map, true); 487 userMovedMap = false; 488 } 489 }, 300) 490 ); 491 } 492 } 493 494 495 function debounce(func, wait) { 496 var timeout; 497 return function () { 498 var context = this, 499 args = arguments; 500 clearTimeout(timeout); 501 timeout = setTimeout(function () {
502 func.apply(context, args); 503 }, wait); 504 }; 505 } 506 507 // Function to add/update user location marker and radius circle 508 function updateUserLocationOnMap(lat, lng, radius, locationText) { 509 if (!window.map || !lat || !lng || !radius) { 510 return; 511 } 512 513 // Validate and normalize coordinates before creating user location marker 514 var validCoords = isValidCoordinate(lat, lng); 515 if (!validCoords) { 516 return; 517 } 518 519 try { 520 // Clear existing user location elements 521 userLocationLayer.clearLayers(); 522 523 // Create user location marker with distinct styling 524 var userLocationIcon = L.divIcon({ 525 iconAnchor: [7.5, 15], 526 popupAnchor: [0, -15], 527 className: 'listeo-user-location-marker', 528 html: '<div class="user-location-container">' + 529 '<div class="user-location-marker">' + 530 '</div>' + 531 '</div>' 532 }); 533 534 userLocationMarker = L.marker([validCoords.lat, validCoords.lng], { 535 icon: userLocationIcon, 536 zIndexOffset: 1000 // Ensure it appears above other markers 537 }); 538 539 // Create radius circle 540 var radiusUnit = listeo_core.radius_unit || 'km'; 541 var radiusInMeters = radius * (radiusUnit === 'km' ? 1000 : 1609.34); // Convert to meters 542 radiusCircle = L.circle([validCoords.lat, validCoords.lng], { 543 radius: radiusInMeters, 544 fillColor: '#007cba', 545 fillOpacity: 0.1, 546 color: '#007cba', 547 weight: 2, 548 opacity: 0.8, 549 dashArray: '5, 10' 550 }); 551 552 // Add to user location layer 553 userLocationLayer.addLayer(userLocationMarker); 554 userLocationLayer.addLayer(radiusCircle); 555 556 // Fit map to include radius circle only on first show and if autofit is enabled 557 if (listeo_core.maps_autofit == "on" && userLocationFirstShow && !userMovedMap) { 558 var bounds = radiusCircle.getBounds(); 559 // Extend bounds to include existing markers if any 560 if (markerArray.length > 0) { 561 var listingsBounds = L.featureGroup(markerArray).getBounds(); 562 bounds.extend(listingsBounds); 563 } 564 window.map.fitBounds(bounds, { padding: [20, 20] }); 565 userLocationFirstShow = false; // Mark that we've done the initial auto-fit 566 } 567 } catch (error) { 568 // Clear any partial elements that may have been created 569 userLocationLayer.clearLayers(); 570 } 571 } 572 573 var allResultsManifest = null; 574 var allResultsManifestLoading = false; 575 var allResultsManifestFailed = false; 576 var allResultsInitialRender = true; 577 var allResultsManifestRendered = false; 578 var activeManifestSubset = null; 579 var activeManifestFilterSignature = ''; 580 var clusterNavigationMove = false; 581 var manifestMapEventsBound = false; 582 var manifestMapRenderTimer = null; 583 var manifestInitialRenderTimer = null; 584 var manifestInitialRenderAttempts = 0; 585 var manifestInitialRenderMap = null; 586 var manifestInitialResizeObserver = null; 587 var manifestInitialVisibilityHandler = null; 588 var manifestInitialWatchersBound = false; 589 var clusterPrefetchTimer = null; 590 // Tier 1: small pre-clustered overview (zoom 0..N) painted immediately; 591 // tier 2: the full manifest, fetched at idle after the overview is on screen. 592 var overviewManifest = null; 593 var overviewManifestLoading = false; 594 var overviewManifestFailed = false; 595 var fullManifestLoadScheduled = false; 596 var fullManifestIntentArmed = false; 597 598 function stopAllResultsManifestInitialWatchers() { 599 clearTimeout(manifestInitialRenderTimer); 600 if (manifestInitialResizeObserver) { 601 manifestInitialResizeObserver.disconnect(); 602 manifestInitialResizeObserver = null; 603 } 604 $(window).off('.listeoManifestInitial'); 605 if (manifestInitialVisibilityHandler) { 606 document.removeEventListener('visibilitychange', manifestInitialVisibilityHandler); 607 manifestInitialVisibilityHandler = null; 608 } 609 manifestInitialWatchersBound = false;
610 } 611 612 function watchAllResultsManifestInitialRender(map) { 613 if (manifestInitialWatchersBound || !map) return; 614 manifestInitialWatchersBound = true; 615 616 var retryRender = function() { 617 if (!allResultsManifestRendered) { 618 renderLoadedAllResultsManifest(window.map || map); 619 } 620 }; 621 622 $(window).on( 623 'load.listeoManifestInitial pageshow.listeoManifestInitial resize.listeoManifestInitial', 624 retryRender 625 ); 626 627 manifestInitialVisibilityHandler = function() { 628 if (!document.hidden) retryRender(); 629 }; 630 document.addEventListener('visibilitychange', manifestInitialVisibilityHandler); 631 632 if (window.ResizeObserver && typeof map.getContainer === 'function') { 633 var container = map.getContainer(); 634 if (container) { 635 manifestInitialResizeObserver = new ResizeObserver(function(entries) { 636 var rect = entries[0] && entries[0].contentRect; 637 if (rect && rect.width > 0 && rect.height > 0) retryRender(); 638 }); 639 manifestInitialResizeObserver.observe(container); 640 } 641 } 642 } 643 644 function getBoundsPayload(bounds) { 645 var ne = bounds.getNorthEast();
646 var sw = bounds.getSouthWest(); 647 return { 648 ne_lat: ne.lat, 649 ne_lng: ne.lng, 650 sw_lat: sw.lat, 651 sw_lng: sw.lng 652 }; 653 } 654 655 function updateListingsForMapBounds(map, triggerResults, clusterCacheToken) { 656 var payload = getBoundsPayload(map.getBounds()); 657 var searchForm = $('#listeo_core-search-form'); 658 searchForm.find('.map-bounds, input[name="search_by_map_move"]').remove(); 659 Object.keys(payload).forEach(function(name) { 660 searchForm.append( 661 '<input type="hidden" class="map-bounds" name="map_bounds[' + name + ']" value="' + payload[name] + '">' 662 ); 663 }); 664 searchForm.append('<input type="hidden" name="search_by_map_move" value="true">'); 665 if (triggerResults) { 666 $('div#listeo-listings-container').triggerHandler('update_results', [ 667 1, 668 false, 669 false, 670 null, 671 clusterCacheToken || '' 672 ]); 673 } 674 } 675 676 function getClusterNavigation(map, group) { 677 var bounds = L.latLngBounds( 678 [group.minLat, group.minLng], 679 [group.maxLat, group.maxLng] 680 ); 681 var center = bounds.getCenter(); 682 var targetZoom = Math.min(map.getZoom() + 2, 18); 683 var useSetView = bounds.getNorthEast().distanceTo(bounds.getSouthWest
683()) < 30; 684 685 if (!useSetView && typeof map._getBoundsCenterZoom === 'function') { 686 var targetView = map._getBoundsCenterZoom(bounds, { 687 padding: [35, 35], 688 maxZoom: targetZoom 689 }); 690 center = targetView.center; 691 targetZoom = targetView.zoom; 692 } 693 694 return { 695 bounds: bounds, 696 center: center, 697 zoom: targetZoom, 698 useSetView: useSetView 699 }; 700 } 701 702 function getViewportBoundsForView(map, center, zoom) { 703 var size = map.getSize(); 704 var halfWidth = size.x / 2; 705 var halfHeight = size.y / 2; 706 var centerPoint = map.project(center, zoom); 707 var northEast = map.unproject(L.point(centerPoint.x + halfWidth, centerPoint.y - halfHeight), zoom); 708 var southWest = map.unproject(L.point(centerPoint.x - halfWidth, centerPoint.y + halfHeight), zoom); 709 return L.latLngBounds(southWest, northEast); 710 } 711 712 function getClusterCacheToken(map, group) { 713 return [ 714 map.getZoom(), 715 group.count, 716 group.minLat.toFixed(5), 717 group.maxLat.toFixed(5), 718 group.minLng.toFixed(5), 719 group.maxLng.toFixed(5) 720 ].join(':'); 721 } 722 723 function prefetchClusterListings(map, group, clusterCacheToken) { 724 var navigation = getClusterNavigation(map, group); 725 var predictedBounds = getViewportBoundsForView(map, navigation.center, navigation.zoom); 726 $('div#listeo-listings-container').triggerHandler('prefetch_results', [ 727 1, 728 false, 729 false, 730 getBoundsPayload(predictedBounds), 731 clusterCacheToken 732 ]); 733 } 734 735 function usesAllResultsManifest() { 736 return listeo_core.map_marker_source === 'all_results' && 737 !!listeo_core.map_all_results_manifest_url && 738 !allResultsManifestFailed; 739 } 740 741 function getOverviewManifestUrl() { 742 if (listeo_core.map_overview_manifest_url) { 743 return String(listeo_core.map_overview_manifest_url); 744 } 745 var url = String(listeo_core.map_all_results_manifest_url || ''); 746 return url.indexOf('listing-points-') === -1 ? '' : url.replace('listing-points-', 'listing-overview-'); 747 } 748 749 function usesOverviewManifest() { 750 return usesAllResultsManifest() && !overviewManifestFailed && !!getOverviewManifestUrl(); 751 } 752 753 function scheduleFullManifestLoad(map) { 754 if (fullManifestLoadScheduled) return; 755 fullManifestLoadScheduled = true; 756 var start = function() { 757 ensureAllResultsManifest(window.map || map); 758 }; 759 if (typeof window.requestIdleCallback === 'function') { 760 window.requestIdleCallback(function() { start(); }, { timeout: 1500 }); 761 } else { 762 setTimeout(start, 300); 763 } 764 } 765 766 // Tier 2 is fetched only after a real person shows up: any trusted input 767 // anywhere on the page (synthetic audits like GTmetrix/Lighthouse never send 768 // one), or, under Save-Data, only a direct map/filter intent. Listeners are 769 // passive, fire once and are removed afterwards. 770 function armFullManifestLoadOnIntent(map) { 771 if (fullManifestIntentArmed || fullManifestLoadScheduled) return; 772 fullManifestIntentArmed = true; 773 774 var container = map && typeof map.getContainer === 'function' ? map.getContainer() : null; 775 var form = document.getElementById('listeo_core-search-form'); 776 var saveData = !!(navigator.connection && navigator.connection.saveData); 777 var cleanups = []; 778 var fire = function(event) { 779 if (event && event.isTrusted === false) return; 780 cleanups.forEach(function(cleanup) { cleanup(); }); 781 cleanups = []; 782 scheduleFullManifestLoad(map); 783 }; 784 var listen = function(target, events, options) { 785 if (!target) return; 786 events.forEach(function(name) { 787 target.addEventListener(name, fire, options); 788 cleanups.push(function() { target.removeEventListener(name, fire, options); }); 789 }); 790 }; 791 792 // Map intent: pointer, touch, wheel or keyboard on the map (covers drag, 793 // pinch, wheel zoom and the zoom buttons). 794 listen(container, ['mouseenter', 'pointerdown', 'touchstart', 'wheel', 'keydown'], { passive: true }); 795 // Filter intent: focusing or changing anything in the search form. 796 listen(form, ['focusin', 'change', 'input'], { passive: true }); 797 // Human presence: first input anywhere (capture also catches inner scrolls). 798 if (!saveData) { 799 listen(document, ['pointermove', 'mousemove', 'touchstart', 'keydown', 'wheel', 'scroll'], { passive: true, capture: true }); 800 } 801 } 802 803 function ensureOverviewManifest(map) { 804 if (!usesOverviewManifest()) { 805 ensureAllResultsManifest(map); 806 return; 807 } 808 if (overviewManifest || overviewManifestLoading) { 809 return; 810 } 811 overviewManifestLoading = true; 812 $.ajax({ 813 url: getOverviewManifestUrl(), 814 dataType: 'json', 815 cache: true 816 }).done(function(data) { 817 if (!data || !data.zooms || typeof data.zooms !== 'object') { 818 overviewManifestFailed = true; 819 return; 820 } 821 overviewManifest = data; 822 renderOverviewManifestMap(window.map || map); 823 }).fail(function() { 824 overviewManifestFailed = true; 825 }).always(function() { 826 overviewManifestLoading = false;
827 var manifestMap = window.map || map; 828 if (overviewManifestFailed) { 829 ensureAllResultsManifest(manifestMap); 830 return; 831 } 832 // Pages with an active, manifest-supported filter (taxonomy archives, 833 // preset searches) can't be painted from the unfiltered overview, so 834 // page markers are showing - fetch the full file right away to avoid 835 // the marker swap on first interaction. Unsupported filters keep the 836 // lazy path: they fall back to server markers anyway. 837 var filterState = getAllResultsFilterState(manifestMap); 838 if (filterState && !isAllResultsFilterEmpty(manifestMap)) { 839 scheduleFullManifestLoad(manifestMap); 840 } else { 841 armFullManifestLoadOnIntent(manifestMap); 842 } 843 }); 844 } 845 846 function isAllResultsFilterEmpty(map) { 847 var state = getAllResultsFilterState(map); 848 return !!state && !state.categories.length && !state.regions.length && !state.types.length && 849 !state.features.length && !state.rating && !state.price && !state.keyword && !state.location; 850 } 851 852 function createManifestClusterIcon(group) { 853 var sizeClass = group.count < 100 ? 'marker-cluster-small' : (group.count < 1000 ? 'marker-cluster-medium' : 'marker-cluster-large'); 854 var roleClass = group.manifestRole ? ' listeo-manifest-' + group.manifestRole + '-cluster' : ''; 855 return L.divIcon({ 856 html: '<div><span>' + group.count.toLocaleString() + '</span></div>', 857 className: 'marker-cluster ' + sizeClass + roleClass, 858 iconSize: L.point(50, 50) 859 }); 860 } 861 862 // Both manifest files carry a shared table of marker icons (one entry per 863 // distinct category icon) and every point stores its index in it, so pins 864 // match the ones the server renders into data-icon on listing cards. 865 // Files written before the icon table existed resolve to the default pin. 866 function getManifestIconHtml(source, index) { 867 var icons = source && Array.isArray(source.icons) ? source.icons : null; 868 var position = parseInt(index, 10); 869 if (icons && isFinite(position) && position >= 0 && position < icons.length && icons[position]) { 870 return String(icons[position]); 871 } 872 return '<i class="sl sl-icon-location"></i>'; 873 } 874 875 function createManifestPointIcon(group, iconHtml) { 876 var pointRoleClass = group.manifestRole ? ' listeo-manifest-' + group.manifestRole + '-marker' : ''; 877 var icon = iconHtml || '<i class="sl sl-icon-location"></i>'; 878 return L.divIcon({ 879 iconAnchor: [20, 51], 880 popupAnchor: [0, -51], 881 className: 'listeo-marker-icon' + pointRoleClass, 882 html: '<div class="marker-container"><div class="marker-card"><div class="front face">' + icon + '</div><div class="back face">' + icon + '</div><div class="marker-arrow"></div></div></div>' 883 }); 884 } 885 886 // Renders the pre-clustered overview for the current zoom. Returns true when a 887 // layer was put on the map. Only used until the full manifest has rendered and 888 // only with an empty filter state (filters need per-point data). 889 function renderOverviewManifestMap(map) { 890 map = map || window.map; 891 if (!map || !map._loaded || !overviewManifest || !usesOverviewManifest() || allResultsManifestRendered) { 892 return false; 893 } 894 if (!isAllResultsFilterEmpty(map)) { 895 return false; 896 } 897 898 if (allResultsInitialRender && listeo_core.maps_autofit === 'on' && 899 Array.isArray(overviewManifest.bounds) && overviewManifest.bounds.length === 4) { 900 var b = overviewManifest.bounds; 901 var overviewBounds = L.latLngBounds([b[0], b[1]], [b[2], b[3]]); 902 if (overviewBounds.isValid()) { 903 allResultsInitialRender = false;
904 // moveend -> scheduleAllResultsManifestRender re-renders at the fitted zoom. 905 map.fitBounds(overviewBounds, { padding: [20, 20] }); 906 } 907 } 908 909 var zoom = map.getZoom(); 910 var maxZoom = parseInt(overviewManifest.max_zoom, 10); 911 if (!isFinite(maxZoom)) maxZoom = 10; 912 var rows = overviewManifest.zooms[String(Math.min(zoom, maxZoom))]; 913 if (zoom > maxZoom || !Array.isArray(rows)) { 914 return false; 915 } 916 917 var viewBounds = map.getBounds().pad(0.25); 918 if (markers && map.hasLayer(markers)) { 919 map.removeLayer(markers); 920 } 921 markers = L.layerGroup(); 922 markerArray = []; 923 924 rows.forEach(function(row) { 925 var lat = parseFloat(row[0]); 926 var lng = parseFloat(row[1]); 927 if (!isFinite(lat) || !isFinite(lng) || !viewBounds.contains([lat, lng])) return; 928 var group = { 929 manifestRole: '', 930 count: parseInt(row[2], 10) || 1, 931 minLat: parseFloat(row[3]), 932 minLng: parseFloat(row[4]), 933 maxLat: parseFloat(row[5]), 934 maxLng: parseFloat(row[6]) 935 }; 936 var layer; 937 if (group.count > 1) { 938 layer = L.marker([lat, lng], { icon: createManifestClusterIcon(group) }); 939 layer.on('mouseover', function() { 940 clearTimeout(clusterPrefetchTimer); 941 var clusterCacheToken = getClusterCacheToken(map, group); 942 clusterPrefetchTimer = setTimeout(function() { 943 prefetchClusterListings(map, group, clusterCacheToken); 944 }, 180); 945 }); 946 layer.on('mouseout', function() { 947 clearTimeout(clusterPrefetchTimer); 948 }); 949 layer.on('click', function() { 950 scheduleFullManifestLoad(map); 951 var clusterCacheToken = getClusterCacheToken(map, group); 952 var navigation = getClusterNavigation(map, group); 953 var navigationFinished = false; 954 var finishNavigation = function() { 955 if (navigationFinished) return; 956 navigationFinished = true; 957 userMovedMap = false; 958 scheduleAllResultsManifestRender(map); 959 updateListingsForMapBounds(map, true, clusterCacheToken); 960 }; 961 map.once('zoomend moveend', finishNavigation); 962 if (navigation.useSetView) {
963 map.setView(navigation.center, navigation.zoom); 964 } else { 965 map.fitBounds(navigation.bounds, { padding: [35, 35], maxZoom: navigation.zoom }); 966 } 967 setTimeout(finishNavigation, 500); 968 }); 969 } else { 970 // Single listing: popup content lives in the full manifest, which replaces 971 // this layer as soon as it arrives. 972 layer = L.marker([lat, lng], { 973 icon: createManifestPointIcon(group, getManifestIconHtml(overviewManifest, row[8])) 974 }); 975 } 976 markers.addLayer(layer); 977 markerArray.push(L.marker([lat, lng])); 978 }); 979 980 map.addLayer(markers); 981 return true; 982 } 983 984 function ensureAllResultsManifest(map) { 985 if (!usesAllResultsManifest() || allResultsManifest || allResultsManifestLoading) { 986 return; 987 } 988 989 watchAllResultsManifestInitialRender(map); 990 allResultsManifestLoading = true; 991 $.ajax({ 992 url: listeo_core.map_all_results_manifest_url, 993 dataType: 'json', 994 cache: true 995 }).done(function(data) { 996 if (!data || !Array.isArray(data.points)) { 997 allResultsManifestFailed = true; 998 stopAllResultsManifestInitialWatchers(); 999 return; 1000 } 1001 allResultsManifest = data; 1002 allResultsManifestLoading = false; 1003 manifestInitialRenderAttempts = 0; 1004 var manifestMap = window.map || map; 1005 renderLoadedAllResultsManifest(manifestMap); 1006 1007 // The cold split-map can have a usable Leaflet viewport before its 1008 // internal `_loaded` flag/whenReady callback settles. A real moveend 1009 // already renders correctly in that state, so run that exact render 1010 // path as soon as the manifest arrives instead of waiting for input. 1011 scheduleAllResultsManifestRender(manifestMap); 1012 }).fail(function() { 1013 allResultsManifestFailed = true; 1014 stopAllResultsManifestInitialWatchers(); 1015 }).always(function() { 1016 allResultsManifestLoading = false; 1017 }); 1018 } 1019 1020 function renderLoadedAllResultsManifest(map) { 1021 if (map) { 1022 manifestInitialRenderMap = map; 1023 } 1024 map = manifestInitialRenderMap || window.map; 1025 if (!map || !allResultsManifest || allResultsManifestFailed || allResultsManifestRendered) { 1026 return; 1027 } 1028 1029 var retry = function() { 1030 if (allResultsManifestRendered || ++manifestInitialRenderAttempts >= 100) return; 1031 clearTimeout(manifestInitialRenderTimer); 1032 manifestInitialRenderTimer = setTimeout(function() { 1033 renderLoadedAllResultsManifest(map); 1034 }, 100); 1035 }; 1036 1037 var container = typeof map.getContainer === 'function' ? map.getContainer() : null; 1038 var size = typeof map.getSize === 'function' ? map.getSize() : null; 1039 if (!map._loaded || !container || !size || size.x < 1 || size.y < 1 || 1040 container.offsetWidth < 1 || container.offsetHeight < 1) { 1041 if (!map._loaded && typeof map.whenReady === 'function') { 1042 map.whenReady(function() { 1043 renderLoadedAllResultsManifest(map); 1044 }); 1045 } 1046 retry(); 1047 return; 1048 } 1049 1050 if (typeof map.invalidateSize === 'function') { 1051 map.invalidateSize({ animate: false, pan: false }); 1052 } 1053 1054 var render = function() { 1055 if (!allResultsManifest || allResultsManifestFailed || allResultsManifestRendered) return; 1056 try { 1057 var points = getManifestRenderPoints(map); 1058 if (points !== null) { 1059 renderAllResultsManifestMap(map, points); 1060 } 1061 if (!allResultsManifestRendered) retry(); 1062 } catch (error) { 1063 allResultsManifestFailed = true; 1064 allResultsManifest = null; 1065 if (!markers || !map.hasLayer(markers)) { 1066 markerArray = []; 1067 markers = false; 1068 listingsMap(map); 1069 } 1070 } 1071 }; 1072 1073 L.Util.requestAnimFrame(function() { 1074 L.Util.requestAnimFrame(function() { 1075 render(); 1076 }); 1077 }); 1078 } 1079 1080 function splitManifestTokens(value) { 1081 return String(value || '').split('|').filter(function(token) { return token !== ''; }); 1082 } 1083 1084 function manifestHasToken(value, wanted) { 1085 wanted = String(wanted || '').toLowerCase(); 1086 return !wanted || splitManifestTokens(value).some(function(token) { 1087 return token.toLowerCase() === wanted; 1088 }); 1089 } 1090 1091 function escapeManifestHtml(value) { 1092 return $('<div>').text(String(value || '')).html(); 1093 } 1094 1095 function getManifestPopupHtml(point) { 1096 var url = escapeManifestHtml(point[11] || '#'); 1097 var image = escapeManifestHtml(point[12] || '');
1098 var title = escapeManifestHtml(point[10] || 'Listing'); 1099 var address = escapeManifestHtml(point[9] || ''); 1100 var rating = parseFloat(point[5] || 0); 1101 var reviews = parseInt(point[13] || 0, 10); 1102 1103 if (image) { 1104 return locationData(url, image, title, '', '', address, rating, reviews, ''); 1105 } 1106 1107 return '<div class="leaflet-listing-content"><div class="listing-title">' + 1108 '<h4><a href="' + url + '">' + title + '</a></h4>' + 1109 '<span>' + address + '</span>' + 1110 '</div></div>'; 1111 } 1112 1113 function getAllResultsFilterSignature() { 1114 var container = $('#listeo-listings-container'); 1115 var fields = $('#listeo_core-search-form').serializeArray().filter(function(field) { 1116 return field.name !== 'search_by_map_move' && field.name.indexOf('map_bounds[') !== 0; 1117 }).map(function(field) { 1118 return field.name + '=' + field.value; 1119 }).sort(); 1120 return fields.join('&') + 1121 '|category=' + String(container.data('category') || '') + 1122 '|region=' + String(container.data('region') || '') + 1123 '|feature=' + String(container.data('feature') || ''); 1124 } 1125 1126 function getAllResultsFilterState(map) { 1127 var form = $('#listeo_core-search-form'); 1128 var serialized = form.serializeArray(); 1129 var radiusValue = ''; 1130 var state = { 1131 categories: [], 1132 regions: [], 1133 types: [], 1134 features: [], 1135 rating: 0, 1136 price: null, 1137 keyword: '', 1138 location: '', 1139 bounds: allResultsInitialRender ? null : map.getBounds() 1140 }; 1141 var supported = true; 1142 1143 serialized.forEach(function(field) { 1144 var name = field.name || ''; 1145 var value = String(field.value || '').trim(); 1146 // "0" is the placeholder option of taxonomy selects (e.g. tax-region); 1147 // PHP treats it as empty(), so the manifest filter must ignore it too. 1148 if (!value || value === '-1' || value === 'any' || (value === '0' && name.indexOf('tax-') === 0)) { 1149 return; 1150 } 1151 1152 if (name.indexOf('tax-listing_category') === 0) { 1153 state.categories.push(value); 1154 } else if (name.indexOf('tax-region') === 0) { 1155 state.regions.push(value); 1156 } else if (name.indexOf('tax-listing_feature') === 0) { 1157 if (value === 'on') { 1158 var featureMatch = name.match(/\[([^\]]+)\]/); 1159 value = featureMatch ? featureMatch[1] : ''; 1160 } 1161 if (value) state.features.push(value); 1162 } else if (name === 'rating-filter') { 1163 state.rating = parseFloat(value) || 0; 1164 } else if (name === '_price_range') { 1165 var range = value.split(',').map(function(part) { return parseFloat(part); }); 1166 if (range.length === 2 && !isNaN(range[0]) && !isNaN(range[1])) { 1167 state.price = range; 1168 } 1169 } else if (name === 'keyword_search') { 1170 state.keyword = value.toLowerCase(); 1171 } else if (name === 'location_search') { 1172 state.location = value.toLowerCase(); 1173 } else if (name === 'listing_type' || name === 'listing_type[]') { 1174 state.types.push(value); 1175 } else if (name === 'search_radius') { 1176 // Only meaningful together with a location - resolved after the loop. 1177 radiusValue = value; 1178 } else if ( 1179 name === 'listeo_keyword_search_website' || 1180 name === '_listeo_keyword_search_interaction' || 1181 name === 'listing_order' || 1182 name === 'search_by_map_move' || 1183 name === 'date_range_mode' || 1184 name === 'ai_search_hp' || 1185 name.indexOf('map_bounds[') === 0 || 1186 name.indexOf('place_viewport[') === 0 || 1187 name === 'place_type' 1188 ) { 1189 return; 1190 } else { 1191 // Unknown field with an active value (date range, AI search, custom 1192 // form fields...) - the manifest cannot reproduce that filter 1193 // client-side, so the current-results markers stay in charge. 1194 supported = false; 1195 } 1196 }); 1197 1198 // An active radius search is geographic; the manifest's plain-text 1199 // location match cannot reproduce it. 1200 if (radiusValue && parseFloat(radiusValue) > 0 && 1201 (state.location || $('#listeo_core-search-form input[name="_geolocation_lat"]').val())) { 1202 supported = false;
1203 } 1204 1205 if (!state.categories.length) { 1206 var fixedCategory = $('#listeo-listings-container').data('category'); 1207 if (fixedCategory) state.categories.push(String(fixedCategory)); 1208 } 1209 if (!state.regions.length) { 1210 var fixedRegion = $('#listeo-listings-container').data('region'); 1211 if (fixedRegion) state.regions.push(String(fixedRegion)); 1212 } 1213 if (!state.features.length) { 1214 var fixedFeature = $('#listeo-listings-container').data('feature'); 1215 if (fixedFeature) state.features.push(String(fixedFeature)); 1216 } 1217 1218 return supported ? state : null; 1219 } 1220 1221 function getFilteredManifestPoints(map, sourcePoints, ignoreBounds) { 1222 var state = getAllResultsFilterState(map); 1223 if (!state) { 1224 return null; 1225 } 1226 1227 if (ignoreBounds) { 1228 state.bounds = null; 1229 } 1230 1231 var points = Array.isArray(sourcePoints) ? sourcePoints : allResultsManifest.points; 1232 return points.filter(function(point) { 1233 var lat = parseFloat(point[1]); 1234 var lng = parseFloat(point[2]); 1235 if (!isFinite(lat) || !isFinite(lng)) return false; 1236 if (state.bounds && !state.bounds.contains([lat, lng])) return false; 1237 if (state.categories.length && !state.categories.some(function(value) { return manifestHasToken(point[3], value); })) return false; 1238 if (state.regions.length && !state.regions.some(function(value) { return manifestHasToken(point[4], value); })) return false; 1239 if (state.types.length && !state.types.some(function(value) { return manifestHasToken(point[14], value); })) return false; 1240 if (state.rating && parseFloat(point[5] || 0) < state.rating) return false; 1241 if (state.price) { 1242 var priceMin = parseFloat(point[6] || 0); 1243 var priceMax = parseFloat(point[7] || point[6] || 0); 1244 if (priceMax < state.price[0] || priceMin > state.price[1]) return false; 1245 } 1246 if (state.features.length && !state.features.every(function(value) { return manifestHasToken(point[8], value); })) return false; 1247 if (state.location && String(point[9] || '').toLowerCase().indexOf(state.location) === -1) return false; 1248 if (state.keyword && String(point[10] || '').toLowerCase().indexOf(state.keyword) === -1) return false; 1249 return true; 1250 }); 1251 } 1252 1253 function getManifestRenderPoints(map) { 1254 var filterSignature = getAllResultsFilterSignature(); 1255 if (activeManifestSubset && activeManifestFilterSignature !== filterSignature) { 1256 activeManifestSubset = null; 1257 activeManifestFilterSignature = ''; 1258 } 1259 1260 return getFilteredManifestPoints( 1261 map, 1262 activeManifestSubset || allResultsManifest.points, 1263 !!activeManifestSubset 1264 ); 1265 } 1266 1267 function clusterManifestPoints(points, zoom, manifestRole) { 1268 var gridSize = parseInt(listeo_core.map_cluster_grid_size, 10) || 80; 1269 var worldSize = 256 * Math.pow(2, zoom); 1270 var groups = {}; 1271 1272 points.forEach(function(point) { 1273 var lat = parseFloat(point[1]); 1274 var lng = parseFloat(point[2]); 1275 var mercatorLat = Math.max(-85.05112878, Math.min(85.05112878, lat)); 1276 var sinLat = Math.sin(mercatorLat * Math.PI / 180); 1277 var pixelX = ((lng + 180) / 360) * worldSize; 1278 var pixelY = (0.5 - Math.log((1 + sinLat) / (1 - sinLat)) / (4 * Math.PI)) * worldSize; 1279 var key = Math.floor(pixelX / gridSize) + ':' + Math.floor(pixelY / gridSize); 1280 1281 if (!groups[key]) { 1282 groups[key] = { 1283 manifestRole: manifestRole || '', 1284 count: 0, 1285 latSum: 0, 1286 lngSum: 0, 1287 minLat: lat, 1288 maxLat: lat, 1289 minLng: lng, 1290 maxLng: lng, 1291 point: point, 1292 points: [] 1293 }; 1294 } 1295 var group = groups[key]; 1296 group.points.push(point); 1297 group.count++; 1298 group.latSum += lat; 1299 group.lngSum += lng; 1300 group.minLat = Math.min(group.minLat, lat); 1301 group.maxLat = Math.max(group.maxLat, lat); 1302 group.minLng = Math.min(group.minLng, lng); 1303 group.maxLng = Math.max(group.maxLng, lng); 1304 }); 1305 1306 return Object.keys(groups).map(function(key) { 1307 return groups[key]; 1308 }); 1309 } 1310 1311 function getManifestRenderGroups(map, focusPoints) {
1312 var focusRole = activeManifestSubset ? 'focus' : ''; 1313 var focusGroups = clusterManifestPoints(focusPoints, map.getZoom(), focusRole); 1314 if (!activeManifestSubset) { 1315 return focusGroups; 1316 } 1317 1318 var focusIds = {}; 1319 activeManifestSubset.forEach(function(point) { 1320 focusIds[String(point[0])] = true; 1321 }); 1322 var backgroundPoints = getFilteredManifestPoints(map, allResultsManifest.points, false); 1323 if (backgroundPoints === null) { 1324 return focusGroups; 1325 } 1326 backgroundPoints = backgroundPoints.filter(function(point) { 1327 return !focusIds[String(point[0])]; 1328 }); 1329 1330 return clusterManifestPoints(backgroundPoints, map.getZoom(), 'background').concat(focusGroups); 1331 } 1332 1333 function clearManifestDrilldown() { 1334 activeManifestSubset = null; 1335 activeManifestFilterSignature = ''; 1336 } 1337 1338 function scheduleAllResultsManifestRender(map) { 1339 if (!usesAllResultsManifest()) { 1340 return; 1341 } 1342 if (!allResultsManifest) { 1343 if (overviewManifest && !allResultsManifestRendered) { 1344 clearTimeout(manifestMapRenderTimer); 1345 manifestMapRenderTimer = setTimeout(function() { 1346 renderOverviewManifestMap(map); 1347 }, 35); 1348 } 1349 return; 1350 } 1351 1352 clearTimeout(manifestMapRenderTimer); 1353 manifestMapRenderTimer = setTimeout(function() { 1354 var points = getManifestRenderPoints(map); 1355 if (points !== null) { 1356 renderAllResultsManifestMap(map, points); 1357 } 1358 }, 35); 1359 } 1360 1361 function bindAllResultsManifestMapEvents(map) { 1362 if (manifestMapEventsBound) { 1363 return; 1364 } 1365 manifestMapEventsBound = true; 1366 1367 map.on('dragstart', function() { 1368 clearManifestDrilldown(); 1369 }); 1370 map.on('zoomstart', function() { 1371 if (!clusterNavigationMove) { 1372 clearManifestDrilldown(); 1373 } 1374 }); 1375 map.on('zoomend moveend', function() { 1376 scheduleAllResultsManifestRender(map); 1377 }); 1378 } 1379 1380 function renderAllResultsManifestMap(map, points) { 1381 if (markers && map.hasLayer(markers)) { 1382 map.removeLayer(markers); 1383 } 1384 markers = L.layerGroup(); 1385 markerArray = []; 1386 var groups = getManifestRenderGroups(map, points); 1387 1388 groups.forEach(function(group) { 1389 var center = [group.latSum / group.count, group.lngSum / group.count]; 1390 var layer; 1391 1392 if (group.count > 1) { 1393 layer = L.marker(center, { 1394 icon: createManifestClusterIcon(group), 1395 zIndexOffset: group.manifestRole === 'focus' ? 1000 : 0 1396 }); 1397 layer.on('mouseover', function() { 1398 clearTimeout(clusterPrefetchTimer); 1399 var clusterCacheToken = getClusterCacheToken(map, group); 1400 clusterPrefetchTimer = setTimeout(function() { 1401 prefetchClusterListings(map, group, clusterCacheToken); 1402 }, 180); 1403 }); 1404 layer.on('mouseout', function() { 1405 clearTimeout(clusterPrefetchTimer); 1406 }); 1407 layer.on('click', function() { 1408 activeManifestSubset = group.points; 1409 activeManifestFilterSignature = getAllResultsFilterSignature(); 1410 clusterNavigationMove = true; 1411 var clusterCacheToken = getClusterCacheToken(map, group); 1412 var navigation = getClusterNavigation(map, group); 1413 var navigationFinished = false; 1414 var finishNavigation = function() { 1415 if (navigationFinished) return; 1416 navigationFinished = true; 1417 clusterNavigationMove = false; 1418 userMovedMap = false; 1419 clearTimeout(manifestMapRenderTimer); 1420 L.Util.requestAnimFrame(function() { 1421 var points = getManifestRenderPoints(map); 1422 if (points !== null) { 1423 renderAllResultsManifestMap(map, points); 1424 } 1425 updateListingsForMapBounds(map, true, clusterCacheToken); 1426 }); 1427 }; 1428 map.once('zoomend moveend', finishNavigation); 1429 if (navigation.useSetView) {
1430 map.setView(navigation.center, navigation.zoom); 1431 } else { 1432 map.fitBounds(navigation.bounds, { padding: [35, 35], maxZoom: navigation.zoom }); 1433 } 1434 setTimeout(finishNavigation, 500); 1435 }); 1436 } else { 1437 layer = L.marker(center, { 1438 icon: createManifestPointIcon(group, getManifestIconHtml(allResultsManifest, group.point && group.point[15])), 1439 zIndexOffset: group.manifestRole === 'focus' ? 1000 : 0 1440 }).bindPopup( 1441 getManifestPopupHtml(group.point), 1442 { maxWidth: 270, className: 'leaflet-infoBox' } 1443 ); 1444 } 1445 1446 markers.addLayer(layer); 1447 markerArray.push(L.marker(center)); 1448 }); 1449 1450 map.addLayer(markers); 1451 allResultsManifestRendered = true; 1452 stopAllResultsManifestInitialWatchers(); 1453 if (allResultsInitialRender && points.length && listeo_core.maps_autofit === 'on') { 1454 var allBounds = L.latLngBounds(points.map(function(point) { 1455 return [parseFloat(point[1]), parseFloat(point[2])]; 1456 })); 1457 if (allBounds.isValid()) { 1458 map.fitBounds(allBounds, { padding: [20, 20] }); 1459 } 1460 } 1461 allResultsInitialRender = false;
1462 } 1463 1464 function listingsMap(map){ 1465 if (usesAllResultsManifest()) { 1466 bindAllResultsManifestMapEvents(map); 1467 ensureOverviewManifest(map); 1468 if (allResultsManifest) { 1469 if (!allResultsManifestRendered) { 1470 renderLoadedAllResultsManifest(map); 1471 } 1472 var manifestPoints = getManifestRenderPoints(map); 1473 if (allResultsManifestRendered && manifestPoints !== null) { 1474 renderAllResultsManifestMap(map, manifestPoints); 1475 return; 1476 } 1477 } else if (overviewManifest && renderOverviewManifestMap(map)) { 1478 return; 1479 } 1480 } 1481 1482 markers = L.markerClusterGroup({ 1483 spiderfyOnMaxZoom: true, 1484 showCoverageOnHover: false, 1485 1486 }); 1487 locations = getMarkers(); 1488 1489 for (var i = 0; i < locations.length; i++) { 1490 1491 // Validate and normalize coordinates before creating marker 1492 var lat = locations[i][2]; 1493 var lng = locations[i][1]; 1494 1495 var validCoords = isValidCoordinate(lat, lng); 1496 if (!validCoords) { 1497 console.warn('Listeo Map: Skipping marker for listing at index ' + i + ' due to invalid coordinates'); 1498 continue; // Skip this marker and continue with the next one 1499 } 1500 1501 try { 1502 var listeoIcon = L.divIcon({ 1503 iconAnchor: [20, 51], // point of the icon which will correspond to marker's location 1504 popupAnchor: [0, -51], 1505 className: 'listeo-marker-icon', 1506 html: '<div class="marker-container">'+ 1507 '<div class="marker-card">'+ 1508 '<div class="front face">' + locations[i][4] + '</div>'+ 1509 '<div class="back face">' + locations[i][4] + '</div>'+ 1510 '<div class="marker-arrow"></div>'+ 1511 '</div>'+ 1512 '</div>' 1513 1514 } 1515 ); 1516 var popupOptions = 1517 { 1518 'maxWidth': '270', 1519 'className' : 'leaflet-infoBox' 1520 } 1521 1522 marker = new L.marker([validCoords.lat, validCoords.lng], { 1523 icon: listeoIcon, 1524 1525 }) 1526 .bindPopup(locations[i][0],popupOptions ); 1527 //.addTo(map); 1528 marker.on('click', function(e){ 1529 //alert('test'); 1530 // L.DomUtil.addClass(marker._icon, 'clicked'); 1531 }); 1532 // map.on('popupopen', function (e) { 1533 // L.DomUtil.addClass(e.popup._source._icon, 'clicked'); 1534 1535 1536 // }).on('popupclose', function (e) { 1537 // if(e.popup){ 1538 // L.DomUtil.removeClass(e.popup._source._icon, 'clicked'); 1539 // } 1540 1541 // }); 1542 markers.addLayer(marker); 1543 1544 markerArray.push(L.marker([validCoords.lat, validCoords.lng])); 1545 } catch (error) { 1546 // Continue with next marker instead of breaking the entire map 1547 continue; 1548 } 1549 } 1550 map.addLayer(markers); 1551 1552 1553 if(markerArray.length > 0 ){ 1554 group = L.featureGroup(markerArray); 1555 if ( 1556 listeo_core.maps_autofit == "on" && 1557 !$('#listeo_core-search-form input[name="search_by_map_move"]') 1558 .length 1559 ) { 1560 map.fitBounds(group.getBounds()); 1561 } 1562 1563 } 1564 } 1565 1566 $( '#listeo-listings-container' ).on( 'update_results_success', function ( event, data ) { 1567 if (window.map && usesAllResultsManifest() && !allResultsManifest && !isAllResultsFilterEmpty(window.map)) { 1568 scheduleFullManifestLoad(window.map); 1569 } 1570 userMovedMap = false; 1571 ignoreMapEvents = true; 1572 setTimeout(function(){ ignoreMapEvents = false; }, 1500); 1573 if(window.map){ 1574 var preserveManifestDrilldown = usesAllResultsManifest() && 1575 allResultsManifest && 1576 activeManifestSubset && 1577 activeManifestFilterSignature === getAllResultsFilterSignature(); 1578 if (!preserveManifestDrilldown) { 1579 map.closePopup(); 1580 if (markers && map.hasLayer(markers)) { 1581 map.removeLayer(markers); 1582 } 1583 markerArray = []; 1584 markers = false;
1585 listingsMap(map); 1586 1587 // With zero result markers there is nothing for maps_autofit to 1588 // fit. Keep the map on the searched location instead of leaving 1589 // it wherever it was (or at the default position). 1590 if ( 1591 markerArray.length === 0 && 1592 data && data.map_center && 1593 isFinite(parseFloat(data.map_center.lat)) && 1594 isFinite(parseFloat(data.map_center.lng)) && 1595 !$('#listeo_core-search-form input[name="search_by_map_move"]').length 1596 ) { 1597 map.flyTo( 1598 [parseFloat(data.map_center.lat), parseFloat(data.map_center.lng)], 1599 Math.max(map.getZoom(), 10) 1600 ); 1601 } 1602 } 1603 1604 // Update user location marker if search data includes location and radius 1605 if (data && data.user_location && data.user_location.lat && data.user_location.lng && data.user_location.radius) { 1606 // Check if this is a new location (different from current) 1607 var isNewLocation = !userLocationMarker || 1608 Math.abs(userLocationMarker.getLatLng().lat - data.user_location.lat) > 0.0001 || 1609 Math.abs(userLocationMarker.getLatLng().lng - data.user_location.lng) > 0.0001; 1610 1611 // Only reset firstShow flag if it's a genuinely new location 1612 if (isNewLocation) { 1613 userLocationFirstShow = true; 1614 } 1615 1616 updateUserLocationOnMap( 1617 data.user_location.lat, 1618 data.user_location.lng, 1619 data.user_location.radius, 1620 data.user_location.address 1621 ); 1622 } else { 1623 // Check form data for location search 1624 var locationSearch = $('#location_search').val(); 1625 var radiusValue = $('.distance-radius').val() || $('#search_radius').val(); 1626 1627 if (locationSearch && radiusValue && (listeo_core.maps_api_server || listeo_core.geoapify_maps_api_server)) { 1628 // Clear user location for now - will be handled by the enhanced AJAX response 1629 // This ensures we don't show stale data while new geocoding happens 1630 } else { 1631 // Clear user location if no search location or radius 1632 userLocationLayer.clearLayers(); 1633 userLocationFirstShow = true; // Reset for next location search 1634 } 1635 } 1636 } 1637 1638 }); 1639 1640 1641 var map_id = document.querySelectorAll('div#map'); 1642 1643 if (map_id.length > 0) { 1644 listingsMap(map); 1645 } 1646 1647 if(listeo_core.map_provider != 'google') { 1648 $('.show-map-button').on('click', function(event) { 1649 event.preventDefault(); 1650 $(".hide-map-on-mobile").toggleClass("map-active"); 1651 var text_enabled = $(this).data('enabled'); 1652 var text_disabled = $(this).data('disabled'); 1653 if( $(".hide-map-on-mobile").hasClass('map-active')){ 1654 $(this).text(text_disabled); 1655 1656 // map.removeLayer(markers); 1657 // markerArray = []; 1658 // markers = false; 1659 // map.closePopup(); 1660 // listingsMap(map); 1661 } else { 1662 $(this).text(text_enabled); 1663 } 1664 }); 1665 } 1666 1667 1668 1669 1670 function submitPropertyMap(){ 1671 1672 window.L_DISABLE_3D = true 1673 1674 var curLocation = [-33.92, 151.25]; // Default location (Sydney, Australia) 1675 1676 if(listeo_core.submitCenterPoint) { 1677 var latlngStr = listeo_core.submitCenterPoint.split(",",2); 1678 var lat = parseFloat(latlngStr[0]); 1679 var lng = parseFloat(latlngStr[1]); 1680 1681 var validCoords = isValidCoordinate(lat, lng); 1682 if (validCoords) { 1683 curLocation = [validCoords.lat, validCoords.lng]; 1684 } 1685 } 1686 1687 if($('#_geolocation_long').val() && $('#_geolocation_lat').val()) { 1688 var userLat = parseFloat($('#_geolocation_lat').val()); 1689 var userLng = parseFloat($('#_geolocation_long').val()); 1690
1691 var validCoords = isValidCoordinate(userLat, userLng); 1692 if (validCoords) { 1693 curLocation = [validCoords.lat, validCoords.lng]; 1694 } 1695 } 1696 var listeoIcon = L.divIcon({ 1697 iconAnchor: [20, 51], // point of the icon which will correspond to marker's location 1698 popupAnchor: [0, -51], 1699 className: 'listeo-marker-icon', 1700 html: '<div class="marker-container no-marker-icon ">'+ 1701 '<div class="marker-card">'+ 1702 '<div class="front face"><i class="fa fa-map-pin"></i></div>'+ 1703 '<div class="back face"><i class="fa fa-map-pin"></i></div>'+ 1704 '<div class="marker-arrow"></div>'+ 1705 '</div>'+ 1706 '</div>' 1707 1708 } 1709 ); 1710 var mapOptions = { 1711 center: curLocation, 1712 zoom: 8, 1713 zoomControl: false, 1714 gestureHandling: false 1715 } 1716 window.submit_map = L.map('submit_map',mapOptions); 1717 1718 1719 var zoomOptions = { 1720 zoomInText: '<i class="fa fa-plus" aria-hidden="true"></i>', 1721 zoomOutText: '<i class="fa fa-minus" aria-hidden="true"></i>', 1722 }; 1723 // Creating zoom control 1724 var zoom = L.control.zoom(zoomOptions); 1725 zoom.addTo(submit_map); 1726 1727 1728 switch(listeo_core.map_provider) { 1729 case 'osm': 1730 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 1731 attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 1732 }).addTo(submit_map); 1733 break; 1734 1735 case 'google': 1736 1737 var googleMutantOptions = { 1738 type: 'roadmap', // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid' 1739 maxZoom: 18 1740 }; 1741 1742 // Add Map ID if configured (for Cloud Styled Maps) 1743 if (listeo_core.google_maps_id && listeo_core.google_maps_id !== '') { 1744 googleMutantOptions.mapId = listeo_core.google_maps_id; 1745 } 1746 1747 whenGoogleMapsReady(function() { 1748 L.gridLayer.googleMutant(googleMutantOptions).addTo(submit_map); 1749 }); 1750 1751 break; 1752 1753 case 'mapbox': 1754 1755 var accessToken = listeo_core.mapbox_access_token; 1756 var mapbox_style_url = listeo_core.mapbox_style_url; 1757 1758 if(listeo_core.mapbox_retina){ 1759 L.tileLayer(mapbox_style_url + accessToken, { 1760 attribution: " © <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> © <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>", 1761 maxZoom: 18, 1762 zoomOffset: -1, 1763 //detectRetina: true, 1764 tileSize: 512, 1765 1766 }).addTo(submit_map); 1767 } else { 1768 1769 L.tileLayer(mapbox_style_url + accessToken, { 1770 attribution: " © <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> © <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>", 1771 maxZoom: 18, 1772 //detectRetina: true, 1773 id: 'mapbox.streets', 1774 }).addTo(submit_map); 1775 } 1776 1777 1778 break; 1779 1780 case 'bing': 1781 L.tileLayer.bing(listeo_core.bing_maps_key).addTo(submit_map) 1782 break; 1783 1784 case 'thunderforest': 1785 var tileUrl = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}{r}.png?apikey='+listeo_core.thunderforest_api_key, 1786 layer = new L.TileLayer(tileUrl, {maxZoom: 18}); 1787 submit_map.addLayer(layer); 1788 break; 1789 1790 case 'here': 1791 L.tileLayer.here({appId: listeo_core.here_app_id, appCode: listeo_core.here_app_code}).addTo(submit_map); 1792 break; 1793 } 1794 1795 1796 1797 1798 submit_map.scrollWheelZoom.disable(); 1799 1800 1801 1802 window.submit_marker = new L.marker(curLocation, { 1803 draggable: 'true', 1804 icon: listeoIcon, 1805 }); 1806 1807 if(listeo_core.address_provider == 'osm'){ 1808 if(listeo_core.country){ 1809 var geocoder = new L.Control.Geocoder.Nominatim( { 1810 geocodingQueryParams: {countrycodes: listeo_core.country} //accept-language: 'en' 1811 }); 1812 } else { 1813 var geocoder = new L.Control.Geocoder.Nominatim(); 1814 } 1815 1816 var output = []; 1817 $("#_address").attr('autocomplete', 'off').after('<div id="leaflet-geocode
1817-cont"><ul></ul></div>'); 1818 // $("#_address").on("focusout",function(e) { 1819 // $("#leaflet-geocode-cont").removeClass('active'); 1820 // }); 1821 $("#_address").on("focusin",function(e) { 1822 $('.type-and-hit-enter').addClass('tip-visible'); 1823 1824 setTimeout(function(){ 1825 $('.type-and-hit-enter').removeClass('tip-visible'); 1826 //....and whatever else you need to do 1827 }, 3000); 1828 }); 1829 $("#_address").on("keydown",function search(e) { 1830 1831 $("#leaflet-geocode-cont").removeHighlight(); 1832 if(e.keyCode == 13) { 1833 1834 var query = $(this).val(); 1835 // if query lenght is less than 3 characters, do not send request 1836 if(query.length < 3){ return false; } 1837 if(query){ 1838 1839 1840 geocoder.geocode(query, function(results) { 1841 1842 for (var i = 0; i < results.length; i++) { 1843 1844 output.push('<li data-latitude="'+results[i].center.lat+'" data-longitude="'+results[i].center.lng+'" >'+results[i].name+'</li>'); 1845 } 1846 output.push('<li class="powered-by-osm">Powered by <strong>OpenStreetMap</strong></li>'); 1847 $("#leaflet-geocode-cont").addClass('active'); 1848 $('#autocomplete-container').addClass("osm-dropdown-active"); 1849 $('#leaflet-geocode-cont ul').html(output); 1850 var txt_to_hl = query.split(' '); 1851 txt_to_hl.forEach(function (item) { 1852 $('#leaflet-geocode-cont ul').highlight(item); 1853 }); 1854 output = []; 1855 }); 1856 } 1857 if ($('#_address:focus').length){ return false; } 1858 } 1859 }); 1860 1861 $(".form-field-_address-container").on( "click", "#leaflet-geocode-cont ul li", function(e) { 1862 1863 var newLatLng = new L.LatLng($(this).data('latitude'), $(this).data('longitude')); 1864 $("#_address").val($(this).text()); 1865 submit_marker.setLatLng(newLatLng).update(); 1866 submit_map.panTo(newLatLng); 1867 $("#_geolocation_lat").val($(this).data('latitude')); 1868 $("#_geolocation_long").val($(this).data('longitude')); 1869 $("#leaflet-geocode-cont").removeClass('active'); 1870 $('#autocomplete-container').removeClass("osm-dropdown-active"); 1871 }); 1872 1873 submit_marker.on('dragend', function(event) { 1874 var position = submit_marker.getLatLng(); 1875 submit_marker.setLatLng(position, { 1876 draggable: 'true' 1877 }).bindPopup(position).update(); 1878 1879 geocoder.reverse(position, submit_map.options.crs.scale(submit_map.getZoom()), function(results) { 1880 if (results && results.length && results[0] && results[0].name) { 1881 $("#_address").val(results[0].name); 1882 } 1883 }); 1884 $("#_geolocation_lat").val(position.lat); 1885 $("#_geolocation_long").val(position.lng).keyup(); 1886 }); 1887 } 1888 1889 1890 1891 $("#_geolocation_lat").change(function() { 1892 if( $("#_geolocation_long").val() ) { 1893 var lat = parseFloat($("#_geolocation_lat").val()); 1894 var lng = parseFloat($("#_geolocation_long").val()); 1895 1896 var validCoords = isValidCoordinate(lat, lng); 1897 if (validCoords) { 1898 var position = [validCoords.lat, validCoords.lng]; 1899 submit_marker.setLatLng(position, { 1900 draggable: 'true' 1901 }).bindPopup(position).update(); 1902 submit_map.panTo(position); 1903 } 1904 } 1905 1906 }); 1907 $("#_geolocation_long").change(function() { 1908 if( $("#_geolocation_lat").val() ) { 1909 var lat = parseFloat($("#_geolocation_lat").val()); 1910 var lng = parseFloat($("#_geolocation_long").val()); 1911
1912 var validCoords = isValidCoordinate(lat, lng); 1913 if (validCoords) { 1914 var position = [validCoords.lat, validCoords.lng]; 1915 submit_marker.setLatLng(position, { 1916 draggable: 'true' 1917 }).bindPopup(position).update(); 1918 submit_map.panTo(position); 1919 } 1920 } 1921 1922 }); 1923 1924 submit_map.addLayer(submit_marker); 1925 1926 1927 }; 1928 1929 var submit_map_cont = document.getElementById('submit_map'); 1930 if (typeof(submit_map_cont) != 'undefined' && submit_map_cont != null) { 1931 submitPropertyMap(); 1932 } 1933 1934 1935 1936 function singleListingMap() { 1937 1938 var lng = parseFloat($( '#singleListingMap' ).data('longitude')); 1939 var lat = parseFloat($( '#singleListingMap' ).data('latitude')); 1940 1941 // Validate and normalize coordinates before initializing map 1942 var validCoords = isValidCoordinate(lat, lng); 1943 if (!validCoords) { 1944 console.error('Listeo Map: Cannot display single listing map - invalid coordinates', {lat: lat, lng: lng}); 1945 return; 1946 } 1947 1948 try { 1949 var singleMapIco = "<i class='"+$('#singleListingMap').data('map-icon')+"'></i>"; 1950 if($('#singleListingMap').data('map-icon-svg')){ 1951 var singleMapIco = $('#singleListingMap').data('map-icon-svg'); 1952 } 1953 1954 var map_single; 1955 var listeoIcon = L.divIcon({ 1956 iconAnchor: [20, 51], // point of the icon which will correspond to marker's location 1957 popupAnchor: [0, -51], 1958 className: 'listeo-marker-icon', 1959 html: '<div class="marker-container no-marker-icon ">'+ 1960 '<div class="marker-card">'+ 1961 '<div class="front face">' + singleMapIco + '</div>'+ 1962 '<div class="back face">' + singleMapIco + '</div>'+ 1963 '<div class="marker-arrow"></div>'+ 1964 '</div>'+ 1965 '</div>' 1966 1967 } 1968 ); 1969 var mapOptions = { 1970 center: [validCoords.lat, validCoords.lng], 1971 zoom: listeo_core.maps_single_zoom, 1972 zoomControl: false, 1973 gestureHandling: false 1974 } 1975 1976 map_single = L.map('singleListingMap',mapOptions); 1977 var zoomOptions = { 1978 zoomInText: '<i class="fa fa-plus" aria-hidden="true"></i>', 1979 zoomOutText: '<i class="fa fa-minus" aria-hidden="true"></i>', 1980 }; 1981 // Creating zoom control 1982 var zoom = L.control.zoom(zoomOptions); 1983 zoom.addTo(map_single); 1984 1985 map_single.scrollWheelZoom.disable(); 1986 if($('#singleListingMap-container').hasClass('circle-point')) { 1987 marker = new L.circleMarker([validCoords.lat, validCoords.lng], { 1988 radius:40 1989 }).addTo(map_single); 1990 } else { 1991 marker = new L.marker([validCoords.lat, validCoords.lng], { 1992 icon: listeoIcon, 1993 }).addTo(map_single); 1994 1995 } 1996 } catch (error) { 1997 console.error('Listeo Map: Error creating single listing map', error); 1998 return; 1999 } 2000 2001 switch(listeo_core.map_provider) { 2002 case 'osm': 2003 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 2004 attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 2005 }).addTo(map_single); 2006 break; 2007 case 'google': 2008 2009 var googleMutantOptions = { 2010 type: 'roadmap', // valid values are 'roadmap', 'satellite', 'terrain' and 'hybrid' 2011 maxZoom: 18 2012 }; 2013 2014 // Add Map ID if configured (for Cloud Styled Maps) 2015 if (listeo_core.google_maps_id && listeo_core.google_maps_id !== '') { 2016 googleMutantOptions.mapId = listeo_core.google_maps_id; 2017 } 2018 2019 whenGoogleMapsReady(function() { 2020 L.gridLayer.googleMutant(googleMutantOptions).addTo(map_single); 2021 }); 2022 2023 break; 2024 2025 2026 case 'mapbox': 2027 var accessToken = listeo_core.mapbox_access_token; 2028 var mapbox_style_url = listeo_core.mapbox_style_url; 2029 2030 if(listeo_core.mapbox_retina){ 2031 L.tileLayer(mapbox_style_url + accessToken, { 2032 attribution: " © <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> © <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>", 2033 maxZoom: 18, 2034 zoomOffset: -1, 2035 //detectRetina: true, 2036 tileSize: 512, 2037 2038 }).addTo(map_single); 2039 } else { 2040 2041 L.tileLayer(mapbox_style_url + accessToken, { 2042 attribution: " © <a href='https://www.mapbox.com/about/maps/'>Mapbox</a> © <a href='http://www.openstreetmap.org/copyright'>OpenStreetMap</a> <strong><a href='https://www.mapbox.com/map-feedback/' target='_blank'>Improve this map</a></strong>", 2043 maxZoom: 18, 2044 //detectRetina: true, 2045 id: 'mapbox.streets', 2046 }).addTo(map_single); 2047 } 2048 2049 2050 break; 2051 2052 case 'bing': 2053 L.tileLayer.bing(listeo_core.bing_maps_key).addTo(map_single) 2054 break; 2055 2056 case 'thunderforest': 2057 var tileUrl = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}{r}.png?apikey='+listeo_core.thunderforest_api_key, 2058 layer = new L.TileLayer(tileUrl, {maxZoom: 18}); 2059 map_single.addLayer(layer); 2060 break; 2061 2062 case 'here': 2063 L.tileLayer.here({appId: listeo_core.here_app_id, appCode: listeo_core.here_app_code}).addTo(map_single); 2064 break; 2065 } 2066 2067 $(window).on('load resize', function () {
2068 setTimeout(function(){ map_single.invalidateSize()}, 100); 2069 }); 2070 } 2071 2072 2073 2074 // Single Listing Map Init 2075 var single_map_cont = document.getElementById('singleListingMap'); 2076 2077 if (typeof(single_map_cont) != 'undefined' && single_map_cont != null) { 2078 2079 singleListingMap(); 2080 2081 2082 } 2083 2084 2085 2086if(listeo_core.address_provider == 'osm'){ 2087 if(listeo_core.country){ 2088 var geocoder = new L.Control.Geocoder.Nominatim( { 2089 geocodingQueryParams: {countrycodes: listeo_core.country} 2090 }); 2091 } else { 2092 var geocoder = new L.Control.Geocoder.Nominatim(); 2093 } 2094 var output = []; 2095 $("#location_search").attr('autocomplete', 'off').after('<div id="leaflet-geocode-cont"><ul></ul></div>') 2096 var liSelected; 2097 var next; 2098 // OSM TIP 2099 $("#location_search") 2100 .on("mouseover",function() { 2101 if ( $(this).val().length < 10 ) { 2102 $('.type-and-hit-enter').addClass('tip-visible'); 2103 } 2104 }).on("mouseout",function(e) { 2105 setTimeout(function(){ 2106 $('.type-and-hit-enter').removeClass('tip-visible'); 2107 }, 350); 2108 }).on("keyup",function(e) { 2109 2110 if ( $(this).val().length < 10 ) { 2111 $('.type-and-hit-enter').addClass('tip-visible'); 2112 } 2113 if ( $(this).val().length > 10 ) { 2114 $('.type-and-hit-enter').removeClass('tip-visible tip-visible-focusin'); 2115 } 2116 2117 if(e.which === 40 || e.which === 38 ) { 2118 2119 2120 } else { 2121 $('#leaflet-geocode-cont ul li.selected').removeClass('selected'); 2122 } 2123 // if(e.which !== 38 ) { 2124 // console.log(e.which); 2125 // //$('#leaflet-geocode-cont ul li.selected').removeClass('selected'); 2126 // } 2127 2128 }).on("keydown",function(e) { 2129 var li = $('#leaflet-geocode-cont ul li'); 2130 if(e.which === 40){ 2131 2132 if(liSelected){ 2133 liSelected.removeClass('selected'); 2134 next = liSelected.next(); 2135 if(next.length > 0){ 2136 liSelected = next.addClass('selected'); 2137 }else{ 2138 liSelected = li.eq(0).addClass('selected'); 2139 } 2140 }else{ 2141 liSelected = li.eq(0).addClass('selected'); 2142 } 2143 }else if(e.which === 38){ 2144 if(liSelected){ 2145 liSelected.removeClass('selected'); 2146 next = liSelected.prev(); 2147 if(next.length > 0){ 2148 liSelected = next.addClass('selected'); 2149 }else{ 2150 liSelected = li.last().addClass('selected'); 2151 } 2152 }else{ 2153 liSelected = li.last().addClass('selected'); 2154 } 2155 } 2156 2157 }); 2158 2159 $("#location_search").on("focusin",function() { 2160 if ( $(this).val().length < 10 ) { 2161 $('.type-and-hit-enter').addClass('tip-visible-focusin'); 2162 } 2163 if ( $(this).val().length > 10 ) { 2164 $('.type-and-hit-enter').removeClass('tip-visible-focusin'); 2165 } 2166 }).on("focusout",function() { 2167 setTimeout(function(){ 2168 $('.type-and-hit-enter').removeClass('tip-visible tip-visible-focusin'); 2169 }, 350); 2170 if( $(this).val() == 0 ) { 2171 $('div#listeo-listings-container' ).triggerHandler( 'update_results', [ 1, false ] ); 2172 } 2173 }); 2174 2175 $(".location .fa-map-marker").on("mouseover",function() { 2176 $('.type-and-hit-enter').removeClass('tip-visible-focusin tip-visible'); 2177 }) 2178 2179 $('.type-and-click-btn').on("click",function search(e) { 2180 2181 var query = $('#_address').val(); 2182 //remove any empty space from start and end of the 'query' string 2183 2184 query = query.replace(/^\s+|\s+$/gm,''); 2185 if (query.length < 3) { 2186 return false;
2187 } 2188 if(query){ 2189 geocoder.geocode(query, function(results) { 2190 2191 for (var i = 0; i < results.length; i++) { 2192 2193 output.push('<li data-latitude="'+results[i].center.lat+'" data-longitude="'+results[i].center.lng+'" >'+results[i].name+'</li>'); 2194 } 2195 output.push('<li class="powered-by-osm">Powered by <strong>OpenStreetMap</strong></li>'); 2196 $("#leaflet-geocode-cont").addClass('active'); 2197 $('#autocomplete-container').addClass("osm-dropdown-active"); 2198 $('#leaflet-geocode-cont ul').html(output); 2199 var txt_to_hl = query.split(' '); 2200 txt_to_hl.forEach(function (item) { 2201 $('#leaflet-geocode-cont ul').highlight(item); 2202 }); 2203 output = []; 2204 }); 2205 } 2206 }); 2207 2208 // Clear viewport fields when location input is cleared/emptied 2209 $("#location_search").on('input change', function() { 2210 var locationValue = $(this).val().trim(); 2211 if (locationValue === '') { 2212 var form = $('#listeo_core-search-form'); 2213 form.find('input[name^="place_viewport"]').remove(); 2214 form.find('input[name="place_type"]').remove(); 2215 } 2216 }); 2217 2218 $("#location_search").on("keydown",function search(e) { 2219 2220 2221 if(e.keyCode == 13) { 2222 if($('#leaflet-geocode-cont ul li.selected').length>0){ 2223 $('#leaflet-geocode-cont ul li.selected').trigger('click').removeClass('selected'); 2224 2225 return; 2226 2227 } 2228 2229 var query = $(this).val(); 2230 if (query.length < 3) { 2231 return false; 2232 } 2233 if(query){ 2234 query = query.replace(/^\s+|\s+$/gm, ""); 2235 geocoder.geocode(query, function(results) { 2236 2237 for (var i = 0; i < results.length; i++) { 2238 var bbox = results[i].bbox || results[i].properties.bbox || null; 2239 var bboxAttr = bbox ? ' data-bbox="'+JSON.stringify(bbox).replace(/"/g, '"')+'"' : ''; 2240 2241 // Determine place type from result properties 2242 var placeType = 'unknown'; 2243 if (results[i].properties) { 2244 var osm_type = results[i].properties.type || results[i].properties.osm_type || ''; 2245 if (osm_type === 'administrative' || results[i].properties.class === 'boundary') { 2246 if (results[i].properties.place_rank <= 4) placeType = 'country'; 2247 else if (results[i].properties.place_rank <= 12) placeType = 'region'; 2248 else placeType = 'city'; 2249 } else if (osm_type === 'city' || osm_type === 'town' || osm_type === 'village') { 2250 placeType = 'city'; 2251 } 2252 } 2253 2254 output.push('<li data-latitude="'+results[i].center.lat+'" data-longitude="'+results[i].center.lng+'" data-place-type="'+placeType+'"'+bboxAttr+'>'+results[i].name+'</li>'); 2255 } 2256 output.push('<li class="powered-by-osm">Powered by <strong>OpenStreetMap</strong></li>'); 2257 $('#leaflet-geocode-cont ul').html(output); 2258 var txt_to_hl = query.split(' '); 2259 txt_to_hl.forEach(function (item) { 2260 $('#leaflet-geocode-cont ul').highlight(item); 2261 }); 2262 $('#autocomplete-container').addClass("osm-dropdown-active"); 2263 $("#leaflet-geocode-cont").addClass('active'); 2264 output = []; 2265 }); 2266 } 2267 } 2268 }); 2269 $("#listeo_core-search-form").on( "click", "#leaflet-geocode-cont ul li", function(e) { 2270 2271 $("#location_search").val($(this).text()); 2272 $("#leaflet-geocode-cont").removeClass('active'); 2273 $('#autocomplete-container').removeClass("osm-dropdown-active"); 2274 2275 var newLatLng = new L.LatLng($(this).data('latitude'), $(this).data('longitude')); 2276 2277 // Capture viewport from OSM bounding box for viewport-based search 2278 var bboxData = $(this).data('bbox'); 2279 var placeType = $(this).data('place-type') || 'unknown'; 2280 var form = $('#listeo_core-search-form'); 2281 2282 // Remove old viewport fields 2283 form.find('input[name^="place_viewport"]').remove(); 2284 form.find('input[name="place_type"]').remove(); 2285 2286 // Handle bbox data - Leaflet Control Geocoder returns LatLngBounds object 2287 if (bboxData) { 2288 var south, north, west, east; 2289 2290 // Check if it's a Leaflet LatLngBounds object (has getSouthWest/getNorthEast methods) 2291 if (typeof bboxData.getSouthWest === 'function' && typeof bboxData.getNorthEast === 'function') { 2292 // Use Leaflet's public API
2293 var sw = bboxData.getSouthWest(); 2294 var ne = bboxData.getNorthEast(); 2295 south = sw.lat; 2296 west = sw.lng; 2297 north = ne.lat; 2298 east = ne.lng; 2299 } 2300 // Fallback: Check if it has private _southWest/_northEast properties 2301 else if (bboxData._southWest && bboxData._northEast) { 2302 south = bboxData._southWest.lat; 2303 west = bboxData._southWest.lng; 2304 north = bboxData._northEast.lat; 2305 east = bboxData._northEast.lng; 2306 } 2307 // Fallback: Check if it's an array (Nominatim format: [south, north, west, east]) 2308 else if (Array.isArray(bboxData) && bboxData.length >= 4) { 2309 south = parseFloat(bboxData[0]); 2310 north = parseFloat(bboxData[1]); 2311 west = parseFloat(bboxData[2]); 2312 east = parseFloat(bboxData[3]); 2313 } 2314 // Fallback: Try parsing as JSON string 2315 else if (typeof bboxData === 'string') { 2316 try { 2317 var parsed = JSON.parse(bboxData); 2318 if (Array.isArray(parsed) && parsed.length >= 4) { 2319 south = parseFloat(parsed[0]); 2320 north = parseFloat(parsed[1]); 2321 west = parseFloat(parsed[2]); 2322 east = parseFloat(parsed[3]); 2323 } 2324 } catch (e) { 2325 console.warn('Listeo Viewport (OSM): Failed to parse bbox string', e); 2326 } 2327 } 2328 2329 // If we successfully extracted coordinates, add them to the form 2330 if (typeof south !== 'undefined' && typeof north !== 'undefined' && 2331 typeof west !== 'undefined' && typeof east !== 'undefined') { 2332 2333 // Convert to our viewport format (ne_lat, ne_lng, sw_lat, sw_lng) 2334 form.append('<input type="hidden" name="place_viewport[ne_lat]" value="' + north + '">'); 2335 form.append('<input type="hidden" name="place_viewport[ne_lng]" value="' + east + '">'); 2336 form.append('<input type="hidden" name="place_viewport[sw_lat]" value="' + south + '">'); 2337 form.append('<input type="hidden" name="place_viewport[sw_lng]" value="' + west + '">'); 2338 } 2339 } 2340 2341 // Add place type for auto mode 2342 form.append('<input type="hidden" name="place_type" value="' + placeType + '">'); 2343 2344 // check if map exists 2345 if(window.map){ 2346 // Only fly to location if autofit is disabled 2347 // When autofit is enabled, fitBounds() will handle positioning based on search results 2348 if(listeo_core.maps_autofit != 'on'){ 2349 map.flyTo(newLatLng, 10); 2350 } 2351 2352 // Update user location marker immediately if radius is set 2353 var radiusValue = $('.distance-radius').val() || $('#search_radius').val(); 2354 if (radiusValue && radiusValue > 0) { 2355 updateUserLocationOnMap( 2356 $(this).data('latitude'), 2357 $(this).data('longitude'), 2358 radiusValue, 2359 $(this).text() 2360 ); 2361 } 2362 } 2363 var target = $('div#listeo-listings-container' ); 2364 target.triggerHandler( 'update_results', [ 1, false ] ); 2365 }); 2366 2367 $('#listeo_core-search-form').on('submit', function(){ 2368 if ($('#location_search:focus').length){ return false;} 2369 }); 2370 2371 if($("#location_search").val()) { 2372 var query = $("#location_search").val() 2373 query = query.replace(/^\s+|\s+$/gm, ""); 2374 geocoder.geocode(query, function(results) { 2375 2376 // Only fly to search location if autofit is disabled 2377 // When autofit is enabled, fitBounds() handles the map positioning 2378 if(map && listeo_core.maps_autofit != 'on' && results && results.length && results[0] && results[0].center){ 2379 map.flyTo(results[0].center, 10); 2380 } 2381 2382 2383 }); 2384 } 2385 2386 2387 2388 2389 var mouse_is_inside = false;
2390 2391 $( "#location_search,#_address,#leaflet-geocode-cont" ).on( "mouseenter", function() { 2392 mouse_is_inside=true; 2393 }); 2394 $( "#location_search,#_address,#leaflet-geocode-cont" ).on( "mouseleave", function() { 2395 mouse_is_inside=false; 2396 }); 2397 2398 $("body").mouseup(function(){ 2399 if(! mouse_is_inside) $("#leaflet-geocode-cont").removeClass('active'); 2400 }); 2401} 2402 2403 $(".geoLocation, #listeo_core-search-form .location a,.main-search-input-item.location a,.form-field-_address-container a").on("click", function (e, autoTriggered) { 2404 e.preventDefault(); 2405 2406 geolocate(!!autoTriggered); 2407 }); 2408 2409 function geolocate(silent) { 2410 2411 if (navigator.geolocation) { 2412 navigator.geolocation.getCurrentPosition(function (position) { 2413 2414 var latitude = position.coords.latitude; 2415 var longitude = position.coords.longitude; 2416 var latlng = L.latLng(latitude, longitude); 2417 2418 // Submit-listing map: reposition the draggable location pin. 2419 // The submit map (submit_map / submit_marker) is a separate map 2420 // from the search map (window.map) and has no listeners tying the 2421 // coordinate fields to the pin, so the "Find my location" button 2422 // used to write the coordinates without ever moving the marker. 2423 // Runs before the address-provider branching so it works for both 2424 // Google and OSM geocoding. Guarded so it is a no-op on any page 2425 // without a submit map. 2426 if (window.submit_marker && window.submit_map) { 2427 $('#_geolocation_lat').val(latitude); 2428 $('#_geolocation_long').val(longitude); 2429 window.submit_marker.setLatLng(latlng).update();
2430 window.submit_map.setView(latlng, 14); 2431 } 2432 2433 if(listeo_core.address_provider == 'google'){ 2434 if(window.map){ 2435 // Geolocate flyTo is programmatic: suppress map-move 2436 // search and drop any previously injected bounds. 2437 autoLocationMapMove = true; 2438 ignoreMapEvents = true; 2439 userMovedMap = false; 2440 $('#listeo_core-search-form') 2441 .find('.map-bounds, input[name="search_by_map_move"]') 2442 .remove(); 2443 // Failsafe: if the flyTo animation is interrupted and 2444 // moveend never fires, do not lock the flags forever. 2445 // Tracked so a newer geolocate cycle cancels the stale 2446 // timer instead of being disarmed by it mid-flight. 2447 clearTimeout(autoLocationFailsafeTimer); 2448 autoLocationFailsafeTimer = setTimeout(function () { 2449 autoLocationMapMove = false; 2450 ignoreMapEvents = false; 2451 }, 3000); 2452 map.flyTo([latitude,longitude],map.getZoom()); 2453 } 2454 $('#_geolocation_lat').val(latitude); 2455 $('#_geolocation_long').val(longitude); 2456 // Maps API loads with loading=async - the reverse 2457 // geocode must wait for it if the user clicked 2458 // geolocate before the API finished loading. 2459 var reverseGeocode = function() { 2460 var pos = new google.maps.LatLng(position.coords.latitude, position.coords.longitude); 2461 var geocoder = new google.maps.Geocoder(); 2462 geocoder.geocode( { 'latLng': pos}, function(results, status) { 2463 if (status == google.maps.GeocoderStatus.OK) { 2464 if (results[1]) { 2465 if($('#location_search').length){ 2466 if($('#location_search').val().length === 0 ) { 2467 $('#location_search').val(results[1].formatted_address); 2468 } 2469 } 2470 $("#_address").val(results[1].formatted_address); 2471 } 2472 2473 } 2474 }); 2475 }; 2476 whenGoogleMapsReady(reverseGeocode); 2477 2478 } else { 2479 if(listeo_core.address_provider == 'osm'){ 2480 if(listeo_core.country){ 2481 var geocoder = new L.Control.Geocoder.Nominatim( { 2482 geocodingQueryParams: {countrycodes: listeo_core.country} //accept-language: 'en' 2483 }); 2484 } else { 2485 var geocoder = new L.Control.Geocoder.Nominatim(); 2486 } 2487 } 2488 // Nominatim resolves the reverse geocode at the zoom we pass: 2489 // ~9 returns a county/region name ("O Condado, Pontevedra"), 2490 // 10 returns the municipality ("Salvaterra de Miño, ..."). 2491 // The zoom used to be taken from map.getZoom(), which during 2492 // the (animated, still pending) flyTo is the archive map's 2493 // initial zoom - default 9 - so "Find my location" filled the 2494 // search field with a REGION instead of the user's town, 2495 // unlike the Google address provider which is always precise. 2496 // Fixed zoom 10 = city/municipality granularity, the right 2497 // scope for a location search field, independent of however 2498 // far the visible map happens to be zoomed out. 2499 var reverseScale = 262144; // 256 * 2^10 -> Nominatim zoom 10 2500 if(window.map){ 2501 // Geolocate flyTo is programmatic: suppress map-move 2502 // search and drop any previously injected bounds. 2503 autoLocationMapMove = true; 2504 ignoreMapEvents = true; 2505 userMovedMap = false;
2506 $('#listeo_core-search-form') 2507 .find('.map-bounds, input[name="search_by_map_move"]') 2508 .remove(); 2509 // Failsafe: if the flyTo animation is interrupted and 2510 // moveend never fires, do not lock the flags forever. 2511 // Tracked so a newer geolocate cycle cancels the stale 2512 // timer instead of being disarmed by it mid-flight. 2513 clearTimeout(autoLocationFailsafeTimer); 2514 autoLocationFailsafeTimer = setTimeout(function () { 2515 autoLocationMapMove = false; 2516 ignoreMapEvents = false; 2517 }, 3000); 2518 map.flyTo([latitude,longitude],10); 2519 geocoder.reverse(latlng, reverseScale, function(results) { 2520 var resultName = (results && results.length && results[0] && results[0].name) ? results[0].name : ''; 2521 if($('#location_search').length){ 2522 if($('#location_search').val().length === 0 && resultName) { 2523 $("#location_search").val(resultName); 2524 } 2525 } 2526 2527 if (resultName) { 2528 $("#_address").val(resultName); 2529 } 2530 $('#_geolocation_lat').val(latitude); 2531 $('#_geolocation_long').val(longitude); 2532 var newLatLng = new L.LatLng(latitude, longitude); 2533 marker.setLatLng(newLatLng).update(); 2534 map.panTo(newLatLng); 2535 var listing_results = $('#listeo-listings-container'); 2536 listing_results.triggerHandler( 'update_results', [ 1, false ] ); 2537 }); 2538 } else { 2539 geocoder.reverse(latlng, reverseScale, function (results) { 2540 var resultName = (results && results.length && results[0] && results[0].name) ? results[0].name : ''; 2541 if ($("#location_search").length) { 2542 if ($("#location_search").val().length === 0 && resultName) { 2543 $("#location_search").val(resultName); 2544 } 2545 } 2546 if (resultName) { 2547 $("#_address").val(resultName); 2548 } 2549 $("#_geolocation_lat").val(latitude); 2550 $("#_geolocation_long").val(longitude); 2551 var listing_results = $("#listeo-listings-container"); 2552 listing_results.triggerHandler("update_results", [ 2553 1, 2554 false, 2555 ]); 2556 }); 2557 2558 } 2559 2560 } 2561 2562 }, function () {
2563 // Geolocation denied, unavailable, or timed out. Without this 2564 // callback nothing happened at all - the map silently stayed 2565 // at the site's configured default center, which customers 2566 // read as "my location was detected somewhere else" (e.g. a 2567 // pin sitting on the default-center city). Tell the visitor 2568 // what happened instead â but only for a MANUAL click on the 2569 // button. The maps_autolocate page-load trigger passes 2570 // silent=true so visitors who denied geolocation aren't 2571 // alerted on every page view. 2572 if (silent) { return; } 2573 var msg = (window.listeo_core && listeo_core.geolocation_failed) 2574 ? listeo_core.geolocation_failed 2575 : 'We could not detect your location. Please allow location access in your browser, or type your location manually.'; 2576 window.alert(msg); 2577 }); 2578 } 2579 } 2580 2581 // Add event handler for radius changes to update user location circle in real time 2582 $(document).on('input change', '.distance-radius, #search_radius', function() { 2583 var radiusValue = $(this).val(); 2584 var locationSearch = $('#location_search').val(); 2585 2586 if (window.map && locationSearch && radiusValue && radiusValue > 0) { 2587 // Update the radius circle if user location is already shown 2588 if (userLocationMarker && radiusCircle) { 2589 var lat = userLocationMarker.getLatLng().lat; 2590 var lng = userLocationMarker.getLatLng().lng; 2591 updateUserLocationOnMap(lat, lng, radiusValue, locationSearch); 2592 } 2593 } 2594 }); 2595 2596 if(listeo_core.maps_autolocate){ 2597 // The extra `true` reaches the click handler as its second argument 2598 // (autoTriggered) so the auto-locate on page load stays SILENT when 2599 // geolocation is denied/unavailable â only a manual click on the 2600 // "Find my location" button shows the failure alert. 2601 $(".geoLocation, #listeo_core-search-form .location a,.main-search-input-item.location a").trigger('click', [true]) 2602 } 2603 }); 2604})(this.jQuery); 2605 2606jQuery.fn.highlight = function(pat) { 2607 function innerHighlight(node, pat) { 2608 var skip = 0; 2609 if (node.nodeType == 3) { 2610 var pos = node.data.toUpperCase().indexOf(pat); 2611 if (pos >= 0) { 2612 var spannode = document.createElement('span'); 2613 spannode.className = 'highlight'; 2614 var middlebit = node.splitText(pos); 2615 var endbit = middlebit.splitText(pat.length); 2616 var middleclone = middlebit.cloneNode(true); 2617 spannode.appendChild(middleclone); 2618 middlebit.parentNode.replaceChild(spannode, middlebit); 2619 skip = 1; 2620 } 2621 } 2622 else if (node.nodeType == 1 && node.childNodes && !/(script|style)/i.test(node.tagName)) { 2623 for (var i = 0; i < node.childNodes.length; ++i) { 2624 i += innerHighlight(node.childNodes[i], pat); 2625 } 2626 } 2627 return skip; 2628 } 2629 return this.each(function() { 2630 innerHighlight(this, pat.toUpperCase()); 2631 }); 2632}; 2633 2634jQuery.fn.removeHighlight = function() { 2635 function newNormalize(node) { 2636 for (var i = 0, children = node.childNodes, nodeCount = children.length; i < nodeCount; i++) { 2637 var child = children[i]; 2638 if (child.nodeType == 1) { 2639 newNormalize(child); 2640 continue; 2641 } 2642 if (child.nodeType != 3) { continue; } 2643 var next = child.nextSibling; 2644 if (next == null || next.nodeType != 3) { continue; } 2645 var combined_text = child.nodeValue + next.nodeValue; 2646 new_node = node.ownerDocument.createTextNode(combined_text); 2647 node.insertBefore(new_node, child); 2648 node.removeChild(child); 2649 node.removeChild(next); 2650 i--; 2651 nodeCount--; 2652 } 2653 } 2654 2655 return this.find("span.highlight").each(function() { 2656 var thisParent = this.parentNode; 2657 thisParent.replaceChild(this.firstChild, this); 2658 newNormalize(thisParent); 2659 }).end(); 2660};
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.