1(function ($, settings) { 2 $.fn.homelocalExplorerMap = function (options, explorerSettings) { 3 const opts = $.extend({}, options); 4 const $container = $(this); 5 6 let map; 7 let propertyMarkers = {}; 8 let mapMarkers = []; 9 let markerClusterer = null; 10 let cachedProperties = []; 11 let currentMarker = null; 12 let currentFocusMarker = null; 13 let mapSearchActive = false; 14 let mapBounds = null; 15 16 const selector = { 17 dateModal: `#hfse-form-date-modal-${opts.id}`, 18 }; 19 20 const classNames = { 21 badgeParentFocus: "hfse-map-badge-parent-focus", 22 }; 23 24 const debounce = (func, timeout = 300) => { 25 let timer; 26 return (...args) => { 27 clearTimeout(timer); 28 timer = setTimeout(() => { 29 func.apply(this, args); 30 }, timeout); 31 }; 32 }; 33 34 // Track the current focused badge globally 35 36 class OverlayMarker extends L.Marker { 37 constructor(position, id, badgeHTML, map) { 38 super(position, { 39 icon: L.divIcon({ 40 html: badgeHTML, 41 iconSize: [60, 40], 42 iconAnchor: [30, 20], 43 }), 44 interactive: true, 45 }); 46 47 this.id_ = id; 48 this.map_ = map; 49 50 // Create tooltip and attach it to the map container 51 const overlayPane = 52 this.map_.getPane("tooltipPane") || this.map_.getPane("overlayPane"); 53 54 this.tooltip_ = L.DomUtil.create( 55 "div", 56 "hfse-map-tooltip", 57 overlayPane, 58 ); 59 this.tooltip_.style.display = "none"; 60 61 // Add event listeners 62 this.on("click", this.showTooltip.bind(this)); 63 this.on("remove", this.onRemove.bind(this)); 64 this._boundUpdateTooltip = this.updateTooltipPosition.bind(this); 65 this.map_.on("zoomend move", this._boundUpdateTooltip); 66 67 this.addTo(map); 68 } 69 70 showTooltip() { 71 if (currentMarker === this) { 72 // Just close it and clear the reference 73 currentMarker.tooltip_.style.display = "none"; 74 currentMarker = null; 75 } else { 76 if (currentMarker) { 77 currentMarker.tooltip_.style.display = "none"; 78 } 79 this.tooltip_.innerHTML = `<div class="hfse-map-tooltip-content">${app.getPopupContent( 80 this.id_, 81 )}</div> 82 <div class="hfse-map-tooltip-close-btn"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="m6 6 20 20M26 6 6 26"></path></svg></div>`; 83 this.tooltip_.style.display = "block"; 84 this.updateTooltipPosition(); 85 // Close tooltip when clicking the close button 86 this.tooltip_ 87 .querySelector(".hfse-map-tooltip-close-btn") 88 .addEventListener("click", () => { 89 this.tooltip_.style.display = "none"; 90 currentMarker = null; 91 }); 92 currentMarker = this; 93 } 94 } 95 96 updateTooltipPosition() { 97 if (this.tooltip_.style.display === "block") { 98 const pane = this._icon._leaflet_pos; 99 this.tooltip_.style.transform = `translate3d(${pane.x - 150}px, ${ 100 pane.y + 30 101 }px, 0px)`; 102 } 103 } 104 105 focus() { 106 if (currentFocusMarker) { 107 currentFocusMarker._icon.classList.remove( 108 classNames.badgeParentFocus, 109 ); 110 } 111 112 // Ensure the badge element is correctly referenced 113 this._icon.classList.add(classNames.badgeParentFocus); 114 115 // Set currentFocusMarker to this badge 116 currentFocusMarker = this; 117 118 const mapBounds = this.map_.getBounds(); 119 if (!mapBounds) return; 120 121 // Calculate a smaller bounding box with a 10% margin
122 const sw = mapBounds.getSouthWest(); 123 const ne = mapBounds.getNorthEast(); 124 125 const latMargin = (ne.lat - sw.lat) * 0.1; // 10% latitude margin 126 const lngMargin = (ne.lng - sw.lng) * 0.1; // 10% longitude margin 127 128 const smallerBounds = L.latLngBounds( 129 [sw.lat + latMargin, sw.lng + lngMargin], 130 [ne.lat - latMargin, ne.lng - lngMargin], 131 ); 132 133 // Get the marker position 134 const markerLatLng = this.getLatLng(); 135 136 // Check if marker is within the smaller bounding box 137 if (!smallerBounds.contains(markerLatLng)) { 138 const currentCenter = this.map_.getCenter(); 139 let newLat = currentCenter.lat; 140 let newLng = currentCenter.lng; 141 142 if (markerLatLng.lng > smallerBounds.getNorthEast().lng) { 143 // Marker is to the right 144 newLng = markerLatLng.lng - (ne.lng - sw.lng) / 2 + lngMargin; 145 } else if (markerLatLng.lng < smallerBounds.getSouthWest().lng) { 146 // Marker is to the left 147 newLng = markerLatLng.lng + (ne.lng - sw.lng) / 2 - lngMargin; 148 } 149 150 if (markerLatLng.lat > smallerBounds.getNorthEast().lat) { 151 // Marker is above 152 newLat = markerLatLng.lat - (ne.lat - sw.lat) / 2 + latMargin; 153 }
153 else if (markerLatLng.lat < smallerBounds.getSouthWest().lat) { 154 // Marker is below 155 newLat = markerLatLng.lat + (ne.lat - sw.lat) / 2 - latMargin; 156 } 157 158 // Pan the map to center on the marker 159 this.map_.panTo([newLat, newLng]); 160 } 161 } 162 163 blur() { 164 if (currentFocusMarker) { 165 currentFocusMarker._icon.classList.remove( 166 classNames.badgeParentFocus, 167 ); 168 } 169 } 170 171 onRemove() { 172 if (currentFocusMarker === this) { 173 currentFocusMarker = null; 174 } 175 176 // Remove tooltip from DOM 177 if (this.tooltip_ && this.tooltip_.parentNode) { 178 this.tooltip_.parentNode.removeChild(this.tooltip_); 179 this.tooltip_ = null; 180 } 181 182 // Remove event listeners 183 this.off(); 184 if (this.map_ && this._boundUpdateTooltip) { 185 this.map_.off("zoomend move", this._boundUpdateTooltip); 186 } 187 188 // Clear references 189 this.map_ = null; 190 this._boundUpdateTooltip = null; 191 } 192 193 cleanup() { 194 if (this.map_) { 195 this.map_.removeLayer(this); 196 } 197 this.onRemove(); 198 } 199 } 200 201 L.Control.CustomControl = L.Control.extend({ 202 onAdd: function (map) { 203 const controlDiv = L.DomUtil.create( 204 "div", 205 "leaflet-control hfse-map-controls", 206 ); 207 208 // Prevents clicks from propagating to the map 209 L.DomEvent.disableClickPropagation(controlDiv); 210 211 const createControlButton = (className, icon, title, onClick) => { 212 const button = L.DomUtil.create( 213 "span", 214 `map-control ${className}`, 215 controlDiv, 216 ); 217 button.setAttribute("hk-icon", icon); 218 button.title = title; 219 button.addEventListener("click", onClick); 220 return button; 221 }; 222 223 // Home Button 224 createControlButton( 225 "control-home", 226 "home", 227 settings.center_map_title, 228 () => { 229 app.closeTooltip(); 230 app.mapFitBounds(); 231 }, 232 ); 233 234 // Previous Marker Button 235 createControlButton("control-prev", "chevron-left", "Previous", () => { 236 if (mapMarkers.length < 1) return; 237 238 if (!currentMarker) { 239 mapMarkers[0].showTooltip(); 240 return; 241 } 242 243 for (let i = 0; i < mapMarkers.length; i++) { 244 if (currentMarker === mapMarkers[i]) { 245 const prevMarker = 246 i === 0 ? mapMarkers[mapMarkers.length - 1] : mapMarkers[i - 1]; 247 prevMarker.showTooltip(); 248 break; 249 } 250 } 251 }); 252 253 // Next Marker Button 254 createControlButton("control-next", "chevron-right", "Next", () => { 255 if (mapMarkers.length < 1) return; 256 257 if (!currentMarker) { 258 mapMarkers[0].showTooltip(); 259 return; 260 } 261 262 for (let i = 0; i < mapMarkers.length; i++) { 263 if (currentMarker === mapMarkers[i]) { 264 const nextMarker = 265 i === mapMarkers.length - 1 ? mapMarkers[0] : mapMarkers[i + 1]; 266 nextMarker.showTooltip(); 267 break; 268 } 269 } 270 }); 271 272 // Fullscreen Button 273 const elementToSendFullscreen = map.getContainer(); 274 const fullScrUI = createControlButton( 275 "control-fullscreen", 276 "expand", 277 "Fullscreen", 278 () => { 279 if (app.isFullscreen(elementToSendFullscreen)) { 280 app.exitFullscreen(); 281 } else { 282 app.requestFullscreen(elementToSendFullscreen); 283 } 284 }, 285 ); 286 287 // Handle fullscreen state changes 288 document.onfullscreenchange = () => { 289 if (app.isFullscreen(elementToSendFullscreen)) { 290 fullScrUI.classList.add("is-fullscreen"); 291 fullScrUI.setAttribute("hk-icon", "shrink"); 292 } else { 293 fullScrUI.classList.remove("is-fullscreen"); 294 fullScrUI.setAttribute("hk-icon", "expand"); 295 } 296 }; 297 298 // Zoom Out Button 299 createControlButton("control-zoomout", "minus", "Zoom Out", () => { 300 map.setZoom(map.getZoom() - 1); 301 }); 302 303 // Zoom In Button 304 createControlButton("control-zoomin", "plus", "Zoom In",
304() => { 305 map.setZoom(map.getZoom() + 1); 306 }); 307 308 if (options.map_search_enabled) { 309 // Add a checkbox for enabling/disabling search on map movement 310 const controlButton = L.DomUtil.create( 311 "span", 312 `map-control control-search`, 313 controlDiv, 314 ); 315 316 const inputContainer = L.DomUtil.create( 317 "div", 318 "map-control-input-container", 319 controlButton, 320 ); 321 322 const inputCheckbox = L.DomUtil.create( 323 "input", 324 "map-control-checkbox", 325 inputContainer, 326 ); 327 328 inputCheckbox.type = "checkbox"; 329 inputCheckbox.id = "hr-map-search-checkbox"; 330 inputCheckbox.checked = false; 331 inputCheckbox.title = options.map_search_label; 332 inputCheckbox.addEventListener("change", (e) => { 333 mapSearchActive = e.target.checked; 334 if (options.onMapSearchStatusChange) { 335 options.onMapSearchStatusChange(e.target.checked); 336 } 337 }); 338 339 const inputLabel = L.DomUtil.create( 340 "label", 341 "map-control-label", 342 inputContainer, 343 ); 344 345 inputLabel.setAttribute("for", "hr-map-search-checkbox"); 346 inputLabel.innerHTML = options.map_search_label; 347 } 348 349 return controlDiv; 350 }, 351 352 onRemove: function (map) { 353 // No special cleanup required 354 }, 355 }); 356 357 const app = { 358 handleMarker: (id, action) => { 359 if (propertyMarkers[id] && propertyMarkers[id][action]) { 360 propertyMarkers[id][action](); 361 } 362 }, 363 364 focusMarker: (id) => app.handleMarker(id, "focus"), 365 366 blurMarker: (id) => app.handleMarker(id, "blur"), 367 368 price: (price, currency) => 369 $.homelocalPrice(price, currency || settings.currency), 370 371 positionMap: () => { 372 const $mapcont = $container.find(".hfse-map-container"); 373 const viewportH = $(window).outerHeight(); 374 const viewportW = $(window).outerWidth(); 375 const header = $(settings.stricky_header_selector); 376 const topGap = header.length > 0 ? header.outerHeight() : 0; 377 378 $mapcont.css( 379 viewportW < 960 380 ? { top: "0px", height: "400px" } 381 : { top: topGap, height: viewportH - topGap }, 382 ); 383 }, 384 385 closeTooltip: () => { 386 if (currentMarker) { 387 currentMarker.tooltip_.style.display = "none"; 388 currentMarker = null; 389 } 390 }, 391 392 setProperties: (properties) => (cachedProperties = properties), 393 394 resetMapMarkers: () => { 395 if (!map) return; 396 397 // Remove each marker properly 398 mapMarkers.forEach((marker) => { 399 try { 400 // Remove from map 401 if (marker._map) { 402 marker._map.removeLayer(marker); 403 } 404 405 // Call onRemove explicitly 406 if (marker.onRemove) { 407 marker.onRemove(); 408 } 409 410 // Remove any remaining DOM elements 411 const markerElement = marker.getElement(); 412 if (markerElement && markerElement.parentNode) { 413 markerElement.parentNode.removeChild(markerElement); 414 } 415 } catch (error) { 416 console.warn("Error removing marker:", error); 417 } 418 }); 419 420 // Clear references 421 mapMarkers = []; 422 propertyMarkers = {}; 423 424 // Reset marker clustering if used 425 if (markerClusterer) { 426 try { 427 markerClusterer.clearLayers(); 428 markerClusterer = null; 429 } catch (error) { 430 console.warn("Error clearing marker cluster:", error); 431 } 432 } 433 434 // Force a map update 435 if (map && map.invalidateSize) { 436 map.invalidateSize(); 437 } 438 }, 439 440 updateMarkers: () => { 441 if (!map) return; 442 443 app.resetMapMarkers(); 444 445 if (!cachedProperties.length) return; 446 447 let latLng = [0, 0]; 448 cachedProperties.some((property) => { 449 const { acf } = property; 450 if (acf.geolocation && acf.geolocation.lat && acf.geolocation.lng) { 451 latLng = [acf.geolocation.lat, acf.geolocation.lng]; 452 return true; // breaks out of the some() loop once a valid latLng is found 453 } 454 return false;
455 }); 456 457 if (!map) { 458 map = L.map("map", { 459 minZoom: parseInt(opts.map_zoom_min, 10), 460 maxZoom: parseInt(opts.map_zoom_max, 10), 461 zoom: parseInt(opts.map_zoom_min, 10), 462 zoomControl: false, // Disables default zoom control 463 attributionControl: false, // Disables default attribution 464 }); 465 L.map("map").setView(latLng, parseInt(opts.map_zoom_min, 10)); 466 467 // Add OpenStreetMap tile layer 468 L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 469 attribution: "© OpenStreetMap contributors", 470 }).addTo(map); 471 } 472 473 cachedProperties.forEach( 474 ({ id, acf, average_rent, lowest_rent, total_rent }) => { 475 const { base_daily_rate, currency, geolocation } = acf; 476 if (!geolocation || !geolocation.lat || !geolocation.lng) return; 477 478 const priceText = (() => { 479 if (opts.full_price === "1" && app.isDateQuery()) { 480 return total_rent ? app.price(total_rent, currency) : ""; 481 } 482 483 switch ( 484 app.isDateQuery() ? opts.with_dates_price : opts.no_dates_price 485 ) { 486 case "average_price": 487 return app.price(average_rent || base_daily_rate, currency); 488 case "base_price": 489 return app.price(base_daily_rate, currency); 490 case "lowest_price": 491 return app.price(lowest_rent || base_daily_rate, currency); 492 default: 493 return ""; 494 } 495 })(); 496 497 const marker = new OverlayMarker( 498 [geolocation.lat, geolocation.lng], // Leaflet uses [lat, lng] array format 499 id, 500 app.getBadgeHTML(priceText), 501 map, 502 ); 503 504 propertyMarkers[id] = marker; 505 mapMarkers.push(marker); 506 }, 507 ); 508 509 if (map && (!mapSearchActive || mapBounds === null)) { 510 app.mapFitBounds(); 511 mapBounds = app.getMapBoundBoxString(); 512 } 513 }, 514 515 getBadgeHTML: (price) => { 516 if (!price) { 517 if (settings.marker_icon) { 518 return ` 519 <img src="${settings.marker_icon}" alt="Property Icon" style="width:${settings.marker_icon_width}px;height:${settings.marker_icon_width}px;" /> 520 `; 521 } else { 522 return ` 523 <div class="hfse-map-badge hfse-map-badge-icon"> 524 <svg fill="#000000" version="1.1" xmlns="http://www.w3.org/2000/svg"
525 viewBox="0 0 486.196 486.196" xml:space="preserve"> 526 <g> 527 <path d="M481.708,220.456l-228.8-204.6c-0.4-0.4-0.8-0.7-1.3-1c-5-4.8-13-5-18.3-0.3l-228.8,204.6c-5.6,5-6,13.5-1.1,19.1 528 c2.7,3,6.4,4.5,10.1,4.5c3.2,0,6.4-1.1,9-3.4l41.2-36.9v7.2v106.8v124.6c0,18.7,15.2,34,34,34c0.3,0,0.5,0,0.8,0s0.5,0,0.8,0h70.6 529 c17.6,0,31.9-14.3,31.9-31.9v-121.3c0-2.7,2.2-4.9,4.9-4.9h72.9c2.7,0,4.9,2.2,4.9,4.9v121.3c0,17.6,14.3,31.9,31.9,31.9h72.2 530 c19,0,34-18.7,34-42.6v-111.2v-34v-83.5l41.2,36.9c2.6,2.3,5.8,3.4,9,3.4c3.7,0,7.4-1.5,10.1-4.5 531 C487.708,233.956,487.208,225.456,481.708,220.456z"/> 532 </g> 533 </svg> 534 </div>`; 535 } 536 } else { 537 return `<div class="hfse-map-badge hfse-map-badge-price">${price}</div>`; 538 } 539 }, 540 541 getPriceHtml: (property) => { 542 const { 543 average_rent, 544 average_rent_discounted, 545 lowest_rent, 546 total_rent, 547 } = property; 548 const { base_daily_rate, currency } = property.acf; 549 550 const replaceText = (text, price) => 551 text.replace("%s", `<strong>${app.price(price, currency)}</strong>`); 552 553 let avgPriceHtml = `<div class="hfse-property-infowindow-price">`; 554 555 if (app.isDateQuery()) { 556 if (options.full_price === "1") { 557 avgPriceHtml += `<span class="hfse-property-price-total">${explorerSettings.total_rent_text.replace( 558 "%s", 559 app.price(total_rent, currency), 560 )}</span>`; 561 562 if (options.full_price_after_tax === "0") { 563 avgPriceHtml += `<div class="hk-text hk-text-small hfse-property-tax-note">${explorerSettings.total_excluding_tax_note}</div>`; 564 } 565 } else { 566 if (average_rent > average_rent_discounted) { 567 avgPriceHtml += ` <del>${app.price( 568 average_rent, 569 currency, 570 )}</del> `; 571 } 572 avgPriceHtml += replaceText( 573 settings.per_night_rent, 574 average_rent_discounted, 575 ); 576 } 577 } else { 578 avgPriceHtml += replaceText( 579 settings.avg_rent_text, 580 average_rent, 581 currency, 582 ); 583 } 584 avgPriceHtml += `</div>`; 585 586 const baseRentHtml = ` 587 <div class="hfse-property-infowindow-price">
588 ${replaceText(settings.base_rent_text, base_daily_rate)} 589 </div>`; 590 591 const lowestRentHtml = ` 592 <div class="hfse-property-infowindow-price"> 593 ${replaceText(settings.lowest_rent_text, lowest_rent)} 594 </div>`; 595 596 const priceToDisplay = app.isDateQuery() 597 ? opts.with_dates_price 598 : opts.no_dates_price; 599 600 if (priceToDisplay === "average_price" && average_rent !== null) { 601 return avgPriceHtml; 602 } else if (priceToDisplay === "lowest_price" && lowest_rent !== null) { 603 return lowestRentHtml; 604 } else if (priceToDisplay !== "none") { 605 return baseRentHtml; 606 } else { 607 return ""; 608 } 609 }, 610 611 getTermsHtml: (property, taxonomy, iconSrc, limit = "3") => { 612 const terms = property[taxonomy]; 613 const visibleItems = 614 terms 615 ?.filter((term) => term.meta?.show_in_card) 616 .slice(0, parseInt(limit || "3", 10)) || []; 617 618 if (!visibleItems.length) return ""; 619 620 const termNames = visibleItems.map((c) => c.name).join(" / "); 621 return ` 622 <div class="hk-flex hk-flex-middle hk-grid-column-small"> 623 <div class="hk-width-auto hk-first-column"> 624 <img alt="" height="18" width="18" src="${iconSrc}" /> 625 </div> 626 <div class="hk-width-auto hk-text-small hk-last-column">${termNames}</div> 627 </div>`; 628 }, 629 630 getPopupContent: (id) => { 631 const property = cachedProperties.find((p) => p.id === id); 632 if (!property || !property.acf.geolocation) return ""; 633 634 const { 635 title, 636 subtitle, 637 image, 638 url, 639 acf: { lat, lng, photos, base_daily_rate, currency }, 640 } = property; 641 642 const coverImage = image.id 643 ? image.sizes["homelocal-thumbnail"] || 644 image.sizes["homelocal-medium"] || 645 image.sizes["large"] 646 : photos.length > 0 647 ? photos[0].sizes["homelocal-thumbnail"] || 648 photos[0].sizes["homelocal-medium"] || 649 photos[0].sizes["large"] 650 : ""; 651 652 const priceHtml = app.getPriceHtml(property); 653 const leftTerms = app.getTermsHtml( 654 property, 655 opts.grid_left_taxonomy, 656 opts.locations_icon, 657 opts.grid_left_limit, 658 ); 659 const rightTerms = app.getTermsHtml( 660 property, 661 opts.grid_right_taxonomy, 662 opts.types_icon, 663 opts.grid_right_limit, 664 ); 665 666 return ` 667 <a class="hk-card hk-card-small hk-card-default hk-display-block hk-text-decoration-none" href="${url}"${ 668 opts.newtab ? ` target="_blank"` : `` 669 }> 670 <div data-src="${coverImage}" hk-img class="hk-inline hk-height-small hk-width-medium hfse-property-infowindow-header"> 671 <div class="hk-overlay hk-position-cover hfse-property-infowindow-header-overlay"></div> 672 </div> 673 <div class="hk-card-body hfse-property-infowindow-body"> 674 <div class="hk-h5 hk-margin-small-bottom hfse-property-title">${ 675 title.rendered 676 }</div> 677 ${ 678 "0" === opts.hide_subtitle && subtitle 679 ? `<div class="hk-text-default hk-margin-remove-top hk-margin-small-bottom hfse-property-subtitle">${subtitle}</div>` 680 : "" 681 } 682 ${priceHtml} 683 <div class="hk-flex hk-grid-small hk-last-column"> 684 ${leftTerms} 685 ${rightTerms} 686 </div> 687 </div> 688 </a>`; 689 }, 690 691 mapPanTo: (latlng, offsetx, offsety) => { 692 if (!map.getProjection()) { 693 console.log("no map projection"); 694 return false; 695 } 696 697 var point1 = map 698 .getProjection() 699 .fromLatLngToPoint( 700 latlng instanceof google.maps.LatLng ? latlng : map.getCenter(), 701 ); 702 var point2 = new google.maps.Point( 703 (typeof offsetx == "number" ? offsetx : 0) / 704 Math.pow(2, map.getZoom()) || 0, 705 (typeof offsety == "number" ? offsety : 0) / 706 Math.pow(2, map.getZoom()) || 0, 707 ); 708 709 map.panTo( 710 map 711 .getProjection() 712 .fromPointToLatLng( 713 new google.maps.Point(point1.x - point2.x, point1.y + point2.y), 714 ), 715 ); 716 }, 717 718 mapFitBounds: () => { 719 // Guard clause: ensure map exists and there are properties 720 if (!map || !cachedProperties.length) { 721 return; 722 } 723 724 try { 725 let bounds = L.latLngBounds(); 726 let hasValidBounds = false;
727 728 // Collect valid coordinates 729 cachedProperties.forEach((property) => { 730 if (property.acf && property.acf.geolocation) { 731 const { lat, lng } = property.acf.geolocation; 732 if (lat && lng && !isNaN(lat) && !isNaN(lng)) { 733 bounds.extend([lat, lng]); 734 hasValidBounds = true; 735 } 736 } 737 }); 738 739 // Only proceed if we have valid bounds 740 if (hasValidBounds) { 741 // Calculate margins (10% of the total size) 742 const sw = bounds.getSouthWest(); 743 const ne = bounds.getNorthEast(); 744 const latMargin = (ne.lat - sw.lat) * 0.1; 745 const lngMargin = (ne.lng - sw.lng) * 0.1; 746 747 // Create new bounds with margins 748 const paddedBounds = L.latLngBounds( 749 [sw.lat - latMargin, sw.lng - lngMargin], 750 [ne.lat + latMargin, ne.lng + lngMargin], 751 ); 752 753 // Use a non-animated version for safety 754 map.fitBounds(paddedBounds, { 755 animate: false, // Disable animation 756 padding: [50, 50], // Add some padding in pixels 757 maxZoom: parseInt(opts.map_zoom_max, 10), 758 duration: 0, // Ensure no duration 759 }); 760 } else { 761 // If no valid bounds, set a default view
762 map.setView([23.8103, 90.4125], parseInt(opts.map_zoom_min, 10), { 763 animate: false, 764 }); 765 } 766 } catch (error) { 767 console.warn("Error in mapFitBounds:", error); 768 // Fallback to default view if something goes wrong 769 map.setView([23.8103, 90.4125], parseInt(opts.map_zoom_min, 10), { 770 animate: false, 771 }); 772 } 773 }, 774 775 getMapBoundBoxString: () => { 776 if (!map) return null; 777 778 const bounds = map.getBounds(); 779 if (!bounds || !bounds.isValid()) { 780 return null; 781 } 782 783 return bounds.toBBoxString(); 784 }, 785 786 isDateQuery: () => { 787 const $checking = $(selector.dateModal).find('input[name="checkin"]'); 788 const $checkout = $(selector.dateModal).find('input[name="checkout"]'); 789 790 if ( 791 $checking.length && 792 $checkout.length && 793 "" !== $checking.val() && 794 "" !== $checkout.val() 795 ) { 796 return true; 797 } 798 799 return false; 800 }, 801 802 isFullscreen: (element) => { 803 return ( 804 (document.fullscreenElement || 805 document.webkitFullscreenElement || 806 document.mozFullScreenElement || 807 document.msFullscreenElement) == element 808 ); 809 }, 810 811 requestFullscreen: (element) => { 812 if (element.requestFullscreen) { 813 element.requestFullscreen(); 814 } else if (element.webkitRequestFullScreen) { 815 element.webkitRequestFullScreen(); 816 } else if (element.mozRequestFullScreen) { 817 element.mozRequestFullScreen(); 818 } else if (element.msRequestFullScreen) { 819 element.msRequestFullScreen(); 820 } 821 }, 822 823 exitFullscreen: () => { 824 if (document.exitFullscreen) { 825 document.exitFullscreen(); 826 } else if (document.webkitExitFullscreen) { 827 document.webkitExitFullscreen(); 828 } else if (document.mozCancelFullScreen) { 829 document.mozCancelFullScreen(); 830 } else if (document.msExitFullscreen) { 831 document.msExitFullscreen(); 832 } 833 }, 834 835 init: () => { 836 map = L.map("map", { 837 minZoom: parseInt(opts.map_zoom_min, 10), 838 maxZoom: parseInt(opts.map_zoom_max, 10), 839 zoom: parseInt(opts.map_zoom_min, 10), 840 zoomControl: false, // Disables default zoom control 841 attributionControl: false, // Disables default attribution 842 }); 843 844 // Add OpenStreetMap tile layer 845 L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { 846 attribution: "© OpenStreetMap contributors", 847 }).addTo(map); 848 849 app.positionMap(); 850 $(window).on("scroll", debounce(app.positionMap, 50)); 851 $(window).on("resize", app.positionMap); 852 853 map.addControl(new L.Control.CustomControl({ position: "topleft" })); 854 855 // listent to map zoom changes. 856 map.on( 857 "zoomend", 858 debounce(() => { 859 if (map && options.onMapPositionChange) { 860 options.onMapPositionChange("zoomend"); 861 } 862 }), 863 ); 864 865 // listen to map move events. 866 map.on( 867 "moveend", 868 debounce(() => { 869 if (map && options.onMapPositionChange) { 870 options.onMapPositionChange("moveend"); 871 } 872 }), 873 ); 874 }, 875 }; 876 877 return app; 878 }; 879})(jQuery, homelocal_explorer_map_settings);
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.