1// The browser's back/forward cache (bfcache) can restore this page exactly 2// as it was in memory -- DOM, JS variables, everything -- without re-running 3// a request. That includes whatever search results/checkout markup were 4// injected via AJAX, with the mptbm_add_to_cart_nonce that was valid *then*. 5// Clicking Book Now on that restored page resubmits stale state (the earlier 6// Book Now click already emptied/rebuilt the cart), which can fail with no 7// visible error. event.persisted is true only when the page came from 8// bfcache, not a normal load -- this file is enqueued on every frontend page 9// (not just the booking one), so the reload is gated on the injected search- 10// result/checkout markup actually being present, rather than firing site-wide 11// on every back-navigation. 12window.addEventListener('pageshow', function (event) { 13 if (event.persisted && document.querySelector('.mptbm_map_search_result, .mptbm_order_summary')) { 14 window.location.reload(); 15 } 16}); 17 18// Admin/MPTBM_Settings_Global.php's 'use_shortest_route' Map API setting (read from 19// the #mptbm_use_shortest_route hidden field get_details.php renders, same pattern as 20// #mptbm_km_or_mile). Off by default: Google's own routes[0] pick balances time and 21// distance (generally what a driver actually navigates). When turned on, request 22// alternatives and price/display whichever route has the smallest distance instead - 23// see MPTBM_Function::get_server_distance() for the matching server-side behavior. 24function mptbm_use_shortest_route() { 25 var el = document.getElementById('mptbm_use_shortest_route'); 26 return !!el && el.value === 'yes'; 27} 28 29function mptbm_shortest_route_index(routes) { 30 var bestIndex = 0; 31 var bestDistance = null; 32 for (var i = 0; i < routes.length; i++) { 33 var legs = routes[i].legs || []; 34 var distance = 0; 35 for (var j = 0; j < legs.length; j++) { 36 distance += legs[j].distance.value; 37 } 38 if (bestDistance === null || distance < bestDistance) { 39 bestDistance = distance; 40 bestIndex = i; 41 } 42 } 43 return bestIndex; 44} 45 46let mptbm_map; 47let mptbm_map_window; 48var mptbm_start_marker = null; 49var mptbm_end_marker = null; 50var mptbm_extra_marker = null; 51 52// Collects the multi-row "Add Extra Stop" values (get_details.php's 53// .mptbm_extra_stops_wrapper) for the search-submit AJAX payloads below. 54// The legacy single #mptbm_map_extra_stop_place input no longer exists in 55// the markup (removed in favor of these rows), so this is the only working 56// source of extra-stop data - without it, jQuery's parent.find('#mptbm_map_extra_stop_place') 57// always resolves to an empty jQuery object and every request silently 58// submitted zero stops regardless of what the customer added. 59function mptbm_collect_extra_stop_places($scope) { 60 var places = []; 61 ($scope || jQuery(document)).find('.mptbm_extra_stop_place_input').each(function () { 62 var val = jQuery(this).val(); 63 if (val) { 64 places.push(val); 65 } 66 }); 67 return places; 68} 69 70function mptbm_collect_extra_stop_coordinates($scope) { 71 var coords = []; 72 ($scope || jQuery(document)).find('.mptbm_extra_stop_coords').each(function () { 73 var val = jQuery(this).val(); 74 if (val) { 75 coords.push(val); 76 } 77 }); 78 return coords; 79} 80 81// OpenStreetMap variables 82var mptbm_osm_map = null; 83var mptbm_osm_markers = []; 84var mptbm_osm_route = null; 85var mptbm_osm_start_marker = null; 86var mptbm_osm_end_marker = null; 87var mptbm_osm_extra_marker = null; 88var mptbm_manual_google_markers = []; 89var mptbm_manual_osm_markers = []; 90var mptbm_manual_google_markers_by_key = {}; 91var mptbm_manual_osm_markers_by_key = {}; 92var mptbm_manual_location_coordinates = {}; 93var mptbm_manual_route_update_token = 0; 94 95// Per-pageload cache of address-search results, keyed by lowercased query 96// text -- re-typing something already searched (or the other field 97// matching the same text) resolves instantly with no network round trip. 98var mptbm_osm_search_cache = {}; 99 100// Base Price global variables 101var mptbm_base_to_pickup_data = { distance: 0, duration: 0 }; 102var mptbm_dropoff_to_base_data = { distance: 0, duration: 0 }; 103 104function mptbm_calculate_base_distances(settings, pickup, dropoff, callback) {
105 if (!settings || !settings.coords || (!pickup && !dropoff)) { 106 if (callback) callback({ distance: 0, duration: 0 }); 107 return; 108 } 109 110 var mapType = document.getElementById('mptbm_map_type'); 111 var isOSM = mapType && mapType.value === 'openstreetmap'; 112 113 if (isOSM) { 114 mptbm_calculate_base_distances_osm(settings, pickup, dropoff, callback); 115 } else { 116 mptbm_calculate_base_distances_google(settings, pickup, dropoff, callback); 117 } 118} 119 120function mptbm_calculate_base_distances_google(settings, pickup, dropoff, callback) { 121 if (typeof google === 'undefined' || typeof google.maps === 'undefined') { 122 if (callback) callback({ distance: 0, duration: 0 }); 123 return; 124 } 125 126 var service = new google.maps.DistanceMatrixService(); 127 var origins = []; 128 var destinations = []; 129 130 // Charge Pickup: Base -> Pickup 131 if (settings.charge_pickup === 'yes' && pickup) { 132 origins.push(settings.coords); 133 destinations.push(pickup); 134 } 135 136 // Charge Dropoff: Dropoff -> Base 137 if (settings.charge_dropoff === 'yes' && dropoff) { 138 origins.push(dropoff); 139 destinations.push(settings.coords); 140 } 141 142 if (origins.length === 0) { 143 if (callback) callback({ distance: 0, duration: 0 }); 144 return; 145 } 146 147 service.getDistanceMatrix({ 148 origins: origins, 149 destinations: destinations, 150 travelMode: google.maps.TravelMode.DRIVING, 151 unitSystem: google.maps.UnitSystem.METRIC, 152 }, function (response, status) { 153 var result = { distance: 0, duration: 0 }; 154 if (status === 'OK') { 155 var idx = 0; 156 // The results are in order of origins. Since each origin has one destination in our mapping: 157 // If we have both pickup and dropoff, origins[0] is Base, destinations[0] is Pickup. 158 // origins[1] is Dropoff, destinations[1] is Base. 159 // Wait, getDistanceMatrix returns a matrix (origins x destinations). 160 // This might result in 4 results if we pass [Base, Dropoff] and [Pickup, Base]. 161 // We only need (Base, Pickup) and (Dropoff, Base). 162 163 // To keep it simple, let's just loop and pick the diagonal if we structured it right, 164 // but DistanceMatrix is more like a grid. 165 166 // Better: use two separate requests or just parse the matrix correctly. 167 // Response.rows[i].elements[j] 168 169 if (settings.charge_pickup === 'yes' && pickup) { 170 var element = response.rows[idx].elements[idx]; 171 if (element.status === 'OK') { 172 result.distance += element.distance.value; 173 result.duration += element.duration.value; 174 } 175 idx++; 176 } 177 if (settings.charge_dropoff === 'yes' && dropoff) { 178 var element = response.rows[idx].elements[idx]; 179 if (element.status === 'OK') { 180 result.distance += element.distance.value; 181 result.duration += element.duration.value; 182 } 183 } 184 } 185 if (callback) callback(result); 186 }); 187} 188 189function mptbm_calculate_base_distances_osm(settings, pickup, dropoff, callback) { 190 // For OSM, we need coordinates. If pickup/dropoff are names, we might need geocoding first. 191 // However, the plugin seems to handle coordinate selection for OSM. 192 193 var baseCoords = settings.coords.split(','); 194 var baseLat = baseCoords[0].trim(); 195 var baseLng = baseCoords[1].trim(); 196 197 // We'll use the OSRM API directly or via a proxy if needed. 198 // For now, let's assume we can use the same project OSRM as used in mptbm_calculate_osm_distance 199 200 var totalDistance = 0; 201 var totalDuration = 0; 202 var pending = 0; 203 204 function handleResult(data) { 205 if (data.code === 'Ok' && data.routes && data.routes.length > 0) { 206 totalDistance += data.routes[0].distance; 207 totalDuration += data.routes[0].duration; 208 } 209 pending--; 210 if (pending === 0 && callback) { 211 callback({ distance: totalDistance, duration: totalDuration }); 212 } 213 } 214 215 // Since OSRM handles point-to-point, we might need two calls if we don't want a single route. 216 // Actually, we can't easily geocode names in JS here without an API. 217 // Let's see if we have coordinates for pickup/dropoff. 218 219 var startCoords = window.mptbm_fixed_zone_start_coords; // If set by mptbm_handle_osm_address_selection 220 var endCoords = window.mptbm_fixed_zone_end_coords; 221 222 if (settings.charge_pickup === 'yes' && startCoords) { 223 pending++; 224 var url = 'https://router.project-osrm.org/route/v1/driving/' + baseLng + ',' + baseLat + ';' + startCoords.longitude + ',' + startCoords.latitude + '?overview=false'; 225 fetch(url).then(r => r.json()).then(handleResult).catch(() => { pending--; if (pending === 0) callback({ distance: totalDistance, duration: totalDuration }); }); 226 } 227 228 if (settings.charge_dropoff === 'yes' && endCoords) { 229 pending++; 230 var url = 'https://router.project-osrm.org/route/v1/driving/' + endCoords.longitude + ',' + endCoords.latitude + ';' + baseLng + ',' + baseLat + '?overview=false'; 231 fetch(url).then(r => r.json()).then(handleResult).catch(() => { pending--; if (pending === 0) callback({ distance: totalDistance, duration: totalDuration }); }); 232 } 233 234 if (pending === 0 && callback) { 235 callback({ distance: 0, duration: 0 }); 236 } 237} 238 239// Function to clean up existing map instance 240function mptbm_cleanup_map() { 241 if (mptbm_map) { 242 // Clear any existing map instances 243 google.maps.event.clearInstanceListeners(mptbm_map); 244 mptbm_map = null; 245 } 246 if (mptbm_map_window) { 247 mptbm_map_window.close(); 248 mptbm_map_window = null; 249 } 250} 251 252// Function to show location validation errors 253function showLocationError(element, message) { 254 // Remove any existing errors for this element 255 var existingError = element.parentElement.querySelector('.mptbm-location-error'); 256 if (existingError) { 257 existingError.remove(); 258 } 259 260 // Add error class to input 261 element.classList.add('mptbm-error-field'); 262 263 // Create error message element 264 var errorDiv = document.createElement('div'); 265 errorDiv.className = 'mptbm-location-error'; 266 errorDiv.style.color = '#dc3545'; 267 errorDiv.style.fontSize = '12px'; 268 errorDiv.style.marginTop = '8px'; 269 errorDiv.style.marginBottom = '10px'; 270 errorDiv.textContent = message; 271 272 // Insert error message after the input 273 element.parentElement.appendChild(errorDiv); 274} 275 276// Function to remove all location errors 277function removeLocationErrors() { 278 var errorFields = document.querySelectorAll('.mptbm-error-field'); 279 errorFields.forEach(function (field) { 280 field.classList.remove('mptbm-error-field'); 281 }); 282 283 var errorMessages = document.querySelectorAll('.mptbm-location-error');
284 errorMessages.forEach(function (error) { 285 error.remove(); 286 }); 287} 288 289function mptbm_resolve_redirect_url(response) { 290 if (!response) { 291 return ''; 292 } 293 294 if (typeof response === 'object') { 295 if (response.redirect_url) { 296 return response.redirect_url; 297 } 298 299 if (response.data && response.data.redirect_url) { 300 return response.data.redirect_url; 301 } 302 303 return ''; 304 } 305 306 if (typeof response === 'string') { 307 var cleaned = response.trim(); 308 309 if (!cleaned) { 310 return ''; 311 } 312 313 try { 314 var parsed = JSON.parse(cleaned); 315 316 if (typeof parsed === 'string') { 317 cleaned = parsed; 318 } else if (parsed && typeof parsed === 'object') { 319 return parsed.redirect_url || (parsed.data && parsed.data.redirect_url) || ''; 320 } 321 } catch (error) { 322 // Keep the raw response when it is already a plain URL. 323 } 324 325 return cleaned 326 .replace(/^"+|"+$/g, '') 327 .replace(/\\\//g, '/'); 328 } 329 330 return ''; 331} 332 333// Add event listeners to clear errors when user starts typing 334jQuery(document).ready(function ($) { 335 // --------------------------------------------------------------------- 336 // Refresh the booking nonce for cached pages. 337 // Full-page caches serve logged-out visitors an HTML page whose embedded 338 // WordPress nonce expires after ~24h, so the search AJAX then fails with a 339 // 403/-1 ("works when logged in, fails when logged out"). admin-ajax.php is 340 // never full-page cached, so we pull a live nonce here and use it for every 341 // request. The user fills the pickup/dropoff/date form before searching, so 342 // this round-trip has completed long before the first search fires. 343 // --------------------------------------------------------------------- 344 window.mptbmNonceReady = (function () { 345 if (typeof mp_ajax_url === 'undefined' || typeof mptbm_ajax === 'undefined') { 346 return $.Deferred().resolve().promise(); 347 } 348 return $.ajax({ 349 type: 'POST', 350 url: mp_ajax_url, 351 data: { action: 'mptbm_refresh_search_nonce' }, 352 dataType: 'json' 353 }).done(function (res) { 354 if (res && res.success && res.data) { 355 if (res.data.search_nonce) { 356 mptbm_ajax.search_nonce = res.data.search_nonce; 357 } 358 if (res.data.add_to_cart_nonce) { 359 mptbm_ajax.add_to_cart_nonce = res.data.add_to_cart_nonce; 360 } 361 } 362 }); 363 })(); 364 365 // Clear errors on input for pickup location 366 $(document).on('input change', '#mptbm_map_start_place, #mptbm_manual_start_place', function () { 367 if (this.classList.contains('mptbm-error-field')) { 368 this.classList.remove('mptbm-error-field'); 369 var errorMsg = this.parentElement.querySelector('.mptbm-location-error'); 370 if (errorMsg) { 371 errorMsg.remove(); 372 } 373 } 374 }); 375 376 // Clear errors on input for dropoff location 377 $(document).on('input change', '#mptbm_map_end_place, #mptbm_manual_end_place', function () { 378 if (this.classList.contains('mptbm-error-field')) { 379 this.classList.remove('mptbm-error-field'); 380 var errorMsg = this.parentElement.querySelector('.mptbm-location-error'); 381 if (errorMsg) { 382 errorMsg.remove(); 383 } 384 } 385 }); 386 387 // Clear errors on input for extra stop location 388 $(document).on('input change', '#mptbm_map_extra_stop_place', function () { 389 if (this.classList.contains('mptbm-error-field')) { 390 this.classList.remove('mptbm-error-field'); 391 var errorMsg = this.parentElement.querySelector('.mptbm-location-error'); 392 if (errorMsg) { 393 errorMsg.remove(); 394 } 395 } 396 }); 397}); 398 399function mptbm_set_cookie_distance_duration(start_place, end_place) { 400 401 402 // Check if OpenStreetMap is active 403 var mapType = document.getElementById('mptbm_map_type'); 404 405 if (mapType && mapType.value === 'openstreetmap') { 406 return false;
407 } 408 409 // Safari compatibility: provide default values 410 start_place = start_place || ""; 411 end_place = end_place || ""; 412 413 // Check if map container exists before initializing 414 var mapContainer = document.getElementById("mptbm_map_area"); 415 if (!mapContainer) { 416 return false; 417 } 418 419 // Check if Google Maps API is loaded 420 if (typeof google === 'undefined' || typeof google.maps === 'undefined') { 421 return false; 422 } 423 424 // Only create a new map if one doesn't exist 425 if (!mptbm_map) { 426 mptbm_map = new google.maps.Map(mapContainer, { 427 mapTypeControl: false, 428 center: mp_lat_lng, 429 zoom: 15, 430 }); 431 } 432 433 // Check if we have enough locations to calculate a route 434 // We need at least start_place and either end_place OR extra_stop 435 var extra_stop = jQuery('#mptbm_map_extra_stop_place').val(); 436 437 // Multi-row "Add Extra Stop" inputs (get_details.php's 438 // .mptbm_extra_stops_wrapper) - read fresh each call, in DOM order, so 439 // added/edited/removed rows are always reflected without needing every 440 // caller of this function to be updated. Google's Directions API 441 // geocodes plain address text itself, so the input's value is enough - 442 // no need to touch the paired coordinate field here. 443 var multi_stop_addresses = []; 444 jQuery('.mptbm_extra_stop_place_input').each(function () { 445 var val = jQuery(this).val(); 446 if (val) { 447 multi_stop_addresses.push(val); 448 } 449 }); 450 451 if (start_place && (end_place || extra_stop || multi_stop_addresses.length)) { 452 var directionsService = new google.maps.DirectionsService(); 453 var directionsRenderer = new google.maps.DirectionsRenderer(); 454 directionsRenderer.setMap(mptbm_map); 455 456 // If dropoff isn't set yet, fall back to the last known stop (a 457 // multi-row stop if any exist, otherwise the legacy single extra 458 // stop) as a temporary destination so a route still draws. 459 var lastMultiStop = multi_stop_addresses.length ? multi_stop_addresses[multi_stop_addresses.length - 1] : ''; 460 var actualDestination = end_place || lastMultiStop || extra_stop; 461 var useExtraAsWaypoint = (end_place || multi_stop_addresses.length) && extra_stop; // Only use legacy extra as waypoint if it's not the destination 462 463 var waypoints = []; 464 if (useExtraAsWaypoint) { 465 waypoints.push({ 466 location: extra_stop, 467 stopover: true 468 }); 469 } 470 multi_stop_addresses.forEach(function (address, index) { 471 // The last multi-stop is being used as the temporary destination 472 // above when dropoff isn't set yet - don't also list it as a waypoint. 473 if (!end_place && index === multi_stop_addresses.length - 1) { 474 return; 475 } 476 waypoints.push({ 477 location: address, 478 stopover: true 479 }); 480 }); 481 482 483 var useShortestRoute = mptbm_use_shortest_route(); 484 var request = { 485 origin: start_place, 486 destination: actualDestination, 487 waypoints: waypoints, 488 travelMode: google.maps.TravelMode.DRIVING, 489 unitSystem: google.maps.UnitSystem.METRIC, 490 }; 491 if (useShortestRoute) { 492 request.provideRouteAlternatives = true; 493 } 494 495 496 var now = new Date(); 497 var time = now.getTime(); 498 var expireTime = time + 3600 * 1000 * 12; 499 now.setTime(expireTime); 500 501 // Safari compatibility: use function instead of arrow function 502 directionsService.route(request, function (result, status) { 503 504 if (status === google.maps.DirectionsStatus.OK) { 505 try { 506 // Same route the server will price when useShortestRoute is on - 507 // see mptbm_shortest_route_index() above. Otherwise routes[0], same 508 // as Google's own default pick. 509 var routeIndex = useShortestRoute ? mptbm_shortest_route_index(result.routes) : 0; 510 511 // Sum all legs of the route (important when waypoints/extra
511stops are used) 512 var totalDistance = 0; 513 var totalDuration = 0; 514 var legs = result.routes[routeIndex].legs; 515 516 517 for (var i = 0; i < legs.length; i++) { 518 totalDistance += legs[i].distance.value; 519 totalDuration += legs[i].duration.value; 520 } 521 522 523 var distance = totalDistance; 524 var duration = totalDuration; 525 526 var kmOrMileElement = document.getElementById("mptbm_km_or_mile"); 527 var kmOrMile = kmOrMileElement ? kmOrMileElement.value : 'km'; 528 var distance_text; 529 var duration_text; 530 531 if (kmOrMile == 'mile') { 532 // Convert distance from meters to miles 533 var distanceInKilometers = distance / 1000; 534 var distanceInMiles = distanceInKilometers * 0.621371; 535 distance_text = distanceInMiles.toFixed(1) + ' miles'; 536 } else { 537 // Convert distance from meters to kilometers 538 var distanceInKilometers = distance / 1000; 539 distance_text = distanceInKilometers.toFixed(1) + ' km'; 540 } 541 542 // Format duration (convert seconds to hours and minutes) 543 var hours = Math.floor(duration / 3600); 544 var minutes = Math.round((duration % 3600) / 60); 545 if (hours > 0) { 546 // duration_text = hours + ' hour' + (hours > 1 ? 's' : '') + ' min'; 547 duration_text = hours + ' hour' + (hours > 1 ? 's' : '') + ' ' + minutes + ' min'; 548 } else { 549 duration_text = minutes + ' min'; 550 } 551 552 // Safari compatibility: set cookies with proper encoding 553 var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax"; 554 document.cookie = "mptbm_distance=" + encodeURIComponent(distance) + cookieOptions; 555 document.cookie = "mptbm_distance_text=" + encodeURIComponent(distance_text) + cookieOptions; 556 document.cookie = "mptbm_duration=" + encodeURIComponent(duration) + cookieOptions; 557 document.cookie = "mptbm_duration_text=" + encodeURIComponent(duration_text) + cookieOptions; 558 559 // Fallback: Update hidden fields for AJAX transmission (when cookies are blocked) 560 var mapArea = jQuery('#mptbm_map_area').closest('.mptbm_transport_search_area'); 561 if (mapArea.length > 0) { 562 if (mapArea.find('input[name="mptbm_hidden_distance"]').length === 0) { 563 mapArea.append('<input type="hidden" name="mptbm_hidden_distance" value="" />'); 564 } 565 if (mapArea.find('input[name="mptbm_hidden_duration"]').length === 0) { 566 mapArea.append('<input type="hidden" name="mptbm_hidden_duration" value="" />'); 567 } 568 569 // Also update our explicit hidden fields if they exist 570 var explicitDistance = document.getElementById('mptbm_calculated_distance'); 571 if (explicitDistance) { 572 explicitDistance.value = distance; 573 } 574 var explicitDuration = document.getElementById('mptbm_calculated_duration'); 575 if (explicitDuration) { 576 explicitDuration.value = duration; 577 } 578 579 // Add hidden inputs for text values as well (needed for cart display) 580 if (mapArea.find('input[name="mptbm_hidden_distance_text"]').length === 0) { 581 mapArea.append('<input type="hidden" name="mptbm_hidden_distance_text" value="" />'); 582 } 583 if (mapArea.find('input[name="mptbm_hidden_duration_text"]').length === 0) { 584 mapArea.append('<input type="hidden" name="mptbm_hidden_duration_text" value="" />'); 585 } 586 587 mapArea.find('input[name="mptbm_hidden_distance"]').val(distance); 588 mapArea.find('input[name="mptbm_hidden_duration"]').val(duration); 589 mapArea.find('input[name="mptbm_hidden_distance_text"]').val(distance_text); 590 mapArea.find('input[name="mptbm_hidden_duration_text"]').val(duration_text); 591 } 592 593 directionsRenderer.setDirections(result); 594 if (useShortestRoute) { 595 directionsRenderer.setRouteIndex(routeIndex); 596 } 597 598 // Update UI elements 599 jQuery(".mptbm_total_distance").html(distance_text); 600 jQuery(".mptbm_total_time").html(duration_text); 601 jQuery(".mptbm_distance_time").slideDown("fast"); 602 mptbm_update_fixed_hours_warning(); 603 604 605 } catch (error) { 606 // Use fallback for Safari 607 if (mptbm_is_safari()) { 608 mptbm_fallback_distance_calculation(start_place, end_place); 609 } 610 } 611 } else { 612 613 // Use fallback for Safari when API fails 614 if (mptbm_is_safari()) { 615 mptbm_fallback_distance_calculation(start_place, end_place); 616 } else { 617 // Show user-friendly error message for other browsers 618 jQuery(".mptbm_total_distance").html("Error calculating distance"); 619 jQuery(".mptbm_total_time").html("Error calculating time"); 620 jQuery(".mptbm_distance_time").slideDown("fast"); 621 } 622 } 623 }); 624 } else if (start_place || end_place) { 625 var place = start_place ? start_place : end_place; 626 mptbm_map_window = new google.maps.InfoWindow(); 627 628 // Check if map container exists before initializing 629 var mapContainer = document.getElementById("mptbm_map_area"); 630 if (!mapContainer) { 631 console.warn("Map container #mptbm_map_area not found. Map initialization skipped."); 632 return false;
633 } 634 635 // Only create a new map if one doesn't exist 636 if (!mptbm_map) { 637 mptbm_map = new google.maps.Map(mapContainer, { 638 center: mp_lat_lng, 639 zoom: 15, 640 }); 641 } 642 643 var request = { 644 query: place, 645 fields: ["name", "geometry"], 646 }; 647 648 // Check if place is a coordinate string (Lat,Lng) to avoid INVALID_REQUEST from Places API 649 // Dictionary-style check or Regex 650 var coordPattern = /^-?\d+(\.\d+)?,\s*-?\d+(\.\d+)?$/; 651 if (coordPattern.test(place)) { 652 // It's a coordinate string. Manually create marker instead of calling Places API. 653 var parts = place.split(','); 654 var lat = parseFloat(parts[0]); 655 var lng = parseFloat(parts[1]); 656 657 if (!isNaN(lat) && !isNaN(lng)) { 658 var location = new google.maps.LatLng(lat, lng); 659 // Mock a place result object 660 var mockPlace = { 661 geometry: { location: location }, 662 name: place 663 }; 664 mptbmCreateMarker(mockPlace); 665 mptbm_map.setCenter(location); 666 return true; 667 } 668 } 669 670 var service = new google.maps.places.PlacesService(mptbm_map); 671 // Safari compatibility: use function instead of arrow function 672 service.findPlaceFromQuery(request, function (results, status) { 673 674 if (status === google.maps.places.PlacesServiceStatus.OK && results) { 675 for (var i = 0; i < results.length; i++) { 676 mptbmCreateMarker(results[i]); 677 } 678 mptbm_map.setCenter(results[0].geometry.location); 679 } else { 680 console.error("Places API error:", status); 681 } 682 }); 683 } else { 684 var directionsRenderer = new google.maps.DirectionsRenderer(); 685 directionsRenderer.setMap(mptbm_map); 686 //document.getElementById('mptbm_map_start_place').focus(); 687 } 688 return true; 689} 690function mptbmCreateMarker(place) { 691 if (!place.geometry || !place.geometry.location) return; 692 693 // Clear existing markers before adding new ones 694 if (mptbm_map && mptbm_map.markers) { 695 mptbm_map.markers.forEach(function (marker) { 696 marker.setMap(null); 697 }); 698 mptbm_map.markers = []; 699 } else if (mptbm_map) { 700 mptbm_map.markers = []; 701 } 702 703 // Safari compatibility: use var instead of const 704 var marker = new google.maps.Marker({ 705 map: mptbm_map, 706 position: place.geometry.location, 707 }); 708 709 // Store marker reference for future clearing 710 if (mptbm_map) { 711 mptbm_map.markers = mptbm_map.markers || []; 712 mptbm_map.markers.push(marker); 713 } 714 715 // Safari compatibility: use function instead of arrow function 716 google.maps.event.addListener(marker, "click", function () { 717 if (mptbm_map_window) { 718 mptbm_map_window.setContent(place.name || ""); 719 mptbm_map_window.open(mptbm_map); 720 } 721 }); 722} 723// Every tab's template (Hourly, Distance, ...) renders its own 724// id="mptbm_map_area" div. Once a second tab has been loaded via AJAX, the 725// DOM holds more than one element sharing that id, so a bare 726// getElementById/querySelector always resolves to whichever one appears 727// first in the DOM -- typically the original (now hidden) tab, not the one 728// actually on screen. Scope the lookup to the visible tab when tabs exist, 729// and fall back to a bare lookup for the tabless [mptbm_booking] shortcode. 730function mptbm_get_current_map_area() { 731 var scoped = document.querySelector('.mptb-tab-content.current #mptbm_map_area'); 732 return scoped || document.getElementById('mptbm_map_area'); 733} 734 735function mptbm_get_current_map_wrap() { 736 var scoped = document.querySelector('.mptb-tab-content.current .mptbm_map_area'); 737 return scoped || document.querySelector('.mptbm_map_area'); 738} 739 740function mptbm_get_manual_map_locations() { 741 var mapWrap = mptbm_get_current_map_wrap(); 742 if (!mapWrap || mapWrap.getAttribute('data-manual-map') !== 'yes') { 743 return []; 744 } 745 746 var dataElement = mapWrap.querySelector('.mptbm-manual-map-locations'); 747 if (!dataElement) { 748 return []; 749 } 750 751 try { 752 var locations = JSON.parse(dataElement.textContent || '[]'); 753 return Array.isArray(locations) ? locations : []; 754 } catch (error) { 755 console.warn('[Manual Route Map] Invalid location data.', error); 756 return []; 757 } 758} 759 760function mptbm_update_manual_map_status(resolved, total, complete) { 761 var mapWrap = mptbm_get_current_map_wrap(); 762 var status = mapWrap ? mapWrap.querySelector('.mptbm_manual_map_status') : null; 763 if (!status) { 764 return; 765 } 766 767 if (complete) { 768 status.textContent = resolved + ' of ' + total + (total === 1 ? ' route location shown' : ' route locations shown'); 769 } else { 770 status.textContent = 'Locating route points⦠' + resolved + '/' + total; 771 } 772} 773 774function mptbm_pick_manual_osm_result(results, label) { 775 if (!Array.isArray(results) || !results.length) { 776 return null; 777 } 778 779 var expectedName = String(label || '').trim().toLowerCase(); 780 var countryField = document.querySelector('[name="mptbm_country"]'); 781 var expectedCountry = countryField && /^[a-z]{2}$/i.test(countryField.value) ? countryField.value.toLowerCase() : ''; 782 var placeTypes = ['city', 'town', 'village', 'state', 'county', 'locality']; 783 784 return results.slice().sort(function (a, b) { 785 var score = function (result) { 786 var address = result.address || {}; 787 var name = String(address.name || '').trim().toLowerCase(); 788 var type = String(address.osm_value || address.type || '').toLowerCase(); 789 var countryCode = String(address.countrycode || '').toLowerCase(); 790 var value = 0; 791 if (expectedCountry && countryCode === expectedCountry) value += 200; 792 if (String(address.osm_key || '').toLowerCase() === 'place') value += 80; 793 if (placeTypes.indexOf(type) !== -1) value += 70; 794 if (name === expectedName) value += 50; 795 if (String(result.display_name || '').toLowerCase().indexOf(expectedName) === 0) value += 20; 796 return value; 797 }; 798 return score(b) - score(a); 799 })[0]; 800} 801 802function mptbm_render_manual_osm_locations() { 803 var locations = mptbm_get_manual_map_locations(); 804 if (!locations.length || !mptbm_osm_map || typeof L === 'undefined') { 805 return; 806 } 807
808 mptbm_manual_osm_markers.forEach(function (marker) { 809 try { marker.remove(); } catch (error) { /* Marker may already belong to a removed map. */ } 810 }); 811 mptbm_manual_osm_markers = []; 812 mptbm_manual_osm_markers_by_key = {}; 813 814 var bounds = L.latLngBounds([]); 815 var resolved = 0; 816 var completed = 0; 817 var fitMap = function () { 818 if (bounds.isValid()) { 819 mptbm_osm_map.fitBounds(bounds, { padding: [45, 45], maxZoom: 11 }); 820 } 821 mptbm_update_manual_map_status(resolved, locations.length, completed >= locations.length); 822 }; 823 var addMarker = function (location, lat, lng) { 824 if (!isFinite(lat) || !isFinite(lng)) { 825 return; 826 } 827 var marker = L.marker([lat, lng], { title: location.label }).addTo(mptbm_osm_map); 828 marker.bindTooltip(location.label, { 829 permanent: true, 830 direction: 'top', 831 offset: [0, -8], 832 className: 'mptbm-manual-location-label' 833 }); 834 marker.bindPopup('<strong>' + jQuery('<div>').text(location.label).html() + '</strong>'); 835 mptbm_manual_osm_markers.push(marker); 836 mptbm_manual_osm_markers_by_key[location.key] = marker; 837 mptbm_manual_location_coordinates[location.key] = { latitude: lat, longitude: lng }; 838 bounds.extend([lat, lng]); 839 resolved++; 840 }; 841 var finishLocation = function () { 842 completed++; 843 fitMap(); 844 if (completed >= locations.length) { 845 setTimeout(function () { mptbm_osm_map.invalidateSize(); fitMap(); }, 50); 846 } 847 }; 848 849 locations.forEach(function (location) { 850 var savedLat = parseFloat(location.lat); 851 var savedLng = parseFloat(location.lng); 852 if (isFinite(savedLat) && isFinite(savedLng)) { 853 addMarker(location, savedLat, savedLng); 854 finishLocation(); 855 return; 856 } 857 858 var query = String(location.label || '').trim(); 859 var cacheKey = query.toLowerCase(); 860 var cached = mptbm_osm_search_cache[cacheKey]; 861 if (cached && cached.length) { 862 var cachedResult = mptbm_pick_manual_osm_result(cached, location.label); 863 addMarker(location, parseFloat(cachedResult.lat), parseFloat(cachedResult.lon)); 864 finishLocation(); 865 return; 866 } 867 868 jQuery.ajax({ 869 url: mptbm_ajax.ajax_url, 870 method: 'GET', 871 data: { action: 'mptbm_osm_search', nonce: mptbm_ajax.osm_nonce, q: query } 872 }).done(function (response) { 873 var results = response && response.success && Array.isArray(response.data) ? response.data : []; 874 mptbm_osm_search_cache[cacheKey] = results; 875 if (results.length) { 876 var bestResult = mptbm_pick_manual_osm_result(results, location.label); 877 addMarker(location, parseFloat(bestResult.lat), parseFloat(bestResult.lon)); 878 } 879 }).always(finishLocation); 880 }); 881} 882 883function mptbm_render_manual_google_locations() { 884 var locations = mptbm_get_manual_map_locations(); 885 var mapContainer = mptbm_get_current_map_area(); 886 if (!locations.length || !mapContainer || typeof google === 'undefined' || !google.maps) { 887 return; 888 } 889 890 if (!mptbm_map || (typeof mptbm_map.getDiv === 'function' && mptbm_map.getDiv() !== mapContainer)) { 891 mptbm_map = new google.maps.Map(mapContainer, { center: mp_lat_lng, zoom: 7, mapTypeControl: false }); 892 } 893 mptbm_manual_google_markers.forEach(function (marker) { marker.setMap(null); }); 894 mptbm_manual_google_markers = []; 895 mptbm_manual_google_markers_by_key = {}; 896 897 var geocoder = new google.maps.Geocoder(); 898 var bounds = new google.maps.LatLngBounds(); 899 var resolved = 0; 900 var completed = 0; 901 var addMarker = function (location, position) { 902 var marker = new google.maps.Marker({ 903 map: mptbm_map, 904 position: position, 905 title: location.label, 906 label: { text: location.label, color: '#172033', fontSize: '12px', fontWeight: '700' } 907 }); 908 var infoWindow = new google.maps.InfoWindow({ content: '<strong>' + jQuery('<div>').text(location.label).html() + '</strong>' }); 909 marker.addListener('click', function () { infoWindow.open({ anchor: marker, map: mptbm_map }); }); 910 mptbm_manual_google_markers.push(marker); 911 mptbm_manual_google_markers_by_key[location.key] = marker; 912 mptbm_manual_location_coordinates[location.key] = { 913 latitude: typeof position.lat === 'function' ? position.lat() : position.lat, 914 longitude: typeof position.lng === 'function' ? position.lng() : position.lng 915 }; 916 bounds.extend(position); 917 resolved++; 918 }; 919 var finishLocation = function () { 920 completed++; 921 if (!bounds.isEmpty()) { 922 mptbm_map.fitBounds(bounds, 45); 923 } 924 mptbm_update_manual_map_status(resolved, locations.length, completed >= locations.length); 925 }; 926 927 locations.forEach(function (location) { 928 var savedLat = parseFloat(location.lat); 929 var savedLng = parseFloat(location.lng); 930 if (isFinite(savedLat) && isFinite(savedLng)) { 931 addMarker(location, { lat: savedLat, lng: savedLng }); 932 finishLocation(); 933 return; 934 } 935 936 var request = { address: location.label }; 937 var restrictCountry = document.querySelector('[name="mptbm_restrict_search_country"]'); 938 var country = document.querySelector('[name="mptbm_country"]'); 939 if (restrictCountry && restrictCountry.value === 'yes' && country && country.value) { 940 request.componentRestrictions = { country: country.value }; 941 } 942 geocoder.geocode(request, function (results, status) { 943 if (status === 'OK' && results && results[0]) { 944 addMarker(location, results[0].geometry.location); 945 } 946 finishLocation(); 947 }); 948 }); 949} 950 951function mptbm_resolve_manual_location(locationKey, locationLabel) { 952 var deferred = jQuery.Deferred(); 953 if (mptbm_manual_location_coordinates[locationKey]) { 954 deferred.resolve(mptbm_manual_location_coordinates[locationKey]); 955 return deferred.promise(); 956 } 957 958 var mapType = document.getElementById('mptbm_map_type'); 959 if (mapType && mapType.value === 'openstreetmap') { 960 var query = String(locationLabel || locationKey || '').trim(); 961 var cacheKey = query.toLowerCase(); 962 var useResults = function (results) { 963 var result = mptbm_pick_manual_osm_result(results, query
963); 964 if (!result) { 965 deferred.resolve(null); 966 return; 967 } 968 var coordinates = { latitude: parseFloat(result.lat), longitude: parseFloat(result.lon) }; 969 mptbm_manual_location_coordinates[locationKey] = coordinates; 970 deferred.resolve(coordinates); 971 }; 972 if (mptbm_osm_search_cache[cacheKey]) { 973 useResults(mptbm_osm_search_cache[cacheKey]); 974 } else { 975 jQuery.ajax({ 976 url: mptbm_ajax.ajax_url, 977 method: 'GET', 978 data: { action: 'mptbm_osm_search', nonce: mptbm_ajax.osm_nonce, q: query } 979 }).done(function (response) { 980 var results = response && response.success && Array.isArray(response.data) ? response.data : []; 981 mptbm_osm_search_cache[cacheKey] = results; 982 useResults(results); 983 }).fail(function () { deferred.resolve(null); }); 984 } 985 } else if (typeof google !== 'undefined' && google.maps) { 986 var request = { address: locationLabel || locationKey }; 987 var country = document.querySelector('[name="mptbm_country"]'); 988 if (country && /^[a-z]{2}$/i.test(country.value)) { 989 request.componentRestrictions = { country: country.value }; 990 } 991 new google.maps.Geocoder().geocode(request, function (results, status) { 992 if (status !== 'OK' || !results || !results[0]) { 993 deferred.resolve(null); 994 return; 995 } 996 var position = results[0].geometry.location; 997 var coordinates = { latitude: position.lat(), longitude: position.lng() }; 998 mptbm_manual_location_coordinates[locationKey] = coordinates; 999 deferred.resolve(coordinates); 1000 }); 1001 } else { 1002 deferred.resolve(null); 1003 } 1004 return deferred.promise(); 1005} 1006 1007function mptbm_update_manual_route_map($searchArea) { 1008 var mapWrap = mptbm_get_current_map_wrap(); 1009 if (!mapWrap || mapWrap.getAttribute('data-manual-map') !== 'yes') { 1010 return; 1011 } 1012 1013 $searchArea = $searchArea && $searchArea.length ? $searchArea : jQuery(mapWrap).closest('.mptbm_transport_search_area'); 1014 var $start = $searchArea.find('#mptbm_manual_start_place'); 1015 var $end = $searchArea.find('#mptbm_manual_end_place'); 1016 var startKey = $start.val(); 1017 var endKey = $end.val(); 1018 if (!startKey || !endKey) { 1019 return; 1020 } 1021 1022 var startLabel = $start.find('option:selected').data('label') || $start.find('option:selected').text(); 1023 var endLabel = $end.find('option:selected').data('label') || $end.find('option:selected').text(); 1024 var updateToken = ++mptbm_manual_route_update_token; 1025 1026 jQuery.when( 1027 mptbm_resolve_manual_location(startKey, startLabel), 1028 mptbm_resolve_manual_location(endKey, endLabel) 1029 ).done(function (startCoordinates, endCoordinates) { 1030 if (updateToken !== mptbm_manual_route_update_token || !startCoordinates || !endCoordinates) { 1031 return; 1032 } 1033 1034 $searchArea.data('mptbm-manual-start-coordinates', startCoordinates); 1035 $searchArea.data('mptbm-manual-end-coordinates', endCoordinates); 1036 var mapType = document.getElementById('mptbm_map_type'); 1037 if (mapType && mapType.value === 'openstreetmap' && mptbm_osm_map) { 1038 mptbm_manual_osm_markers.forEach(function (marker) { 1039 var element = marker.getElement && marker.getElement(); 1040 if (element) element.classList.remove('mptbm-manual-route-start', 'mptbm-manual-route-end'); 1041 }); 1042 mptbm_osm_start_marker = mptbm_manual_osm_markers_by_key[startKey] || L.marker([startCoordinates.latitude, startCoordinates.longitude]).addTo(mptbm_osm_map); 1043 mptbm_osm_end_marker = mptbm_manual_osm_markers_by_key[endKey] || L.marker([endCoordinates.latitude, endCoordinates.longitude]).addTo(mptbm_osm_map); 1044 var startElement = mptbm_osm_start_marker.getElement && mptbm_osm_start_marker.getElement(); 1045 var endElement = mptbm_osm_end_marker.getElement && mptbm_osm_end_marker.getElement(); 1046 if (startElement) startElement.classList.add('mptbm-manual-route-start'); 1047 if (endElement) endElement.classList.add('mptbm-manual-route-end'); 1048 mptbm_calculate_osm_distance(); 1049 } else if (typeof google !== 'undefined' && google.maps && mptbm_map) { 1050 mptbm_start_marker = mptbm_manual_google_markers_by_key[startKey] || new google.maps.Marker({ 1051 map: mptbm_map, 1052 position: { lat: startCoordinates.latitude, lng: startCoordinates.longitude }, 1053 title: startLabel 1054 }); 1055 mptbm_end_marker = mptbm_manual_google_markers_by_key[endKey] || new google.maps.Marker({ 1056 map: mptbm_map, 1057 position: { lat: endCoordinates.latitude, lng: endCoordinates.longitude }, 1058 title: endLabel 1059 }); 1060 mptbm_calculate_google_route_from_markers(); 1061 } 1062 }); 1063} 1064
1065function mptbm_map_area_init() { 1066 1067 // Check if map container exists and is visible before initializing 1068 var mapContainer = mptbm_get_current_map_area(); 1069 if (!mapContainer) { 1070 console.warn("[Map Init] Map container #mptbm_map_area not found. Skipping map initialization."); 1071 return false; 1072 } 1073 1074 // Check if the map container is visible (not hidden by CSS) 1075 var mapArea = mptbm_get_current_map_wrap(); 1076 if (mapArea && mapArea.style.display === 'none') { 1077 // If map is hidden, we still want to initialize address search for OSM 1078 var mapType = document.getElementById('mptbm_map_type'); 1079 if (mapType && mapType.value === 'openstreetmap') { 1080 mptbm_init_osm_address_search(); 1081 } 1082 console.warn("[Map Init] Map area is hidden. Map rendering skipped, but address search may continue."); 1083 return false; 1084 } 1085 1086 // Check map type setting 1087 var mapType = document.getElementById('mptbm_map_type'); 1088 1089 if (!mapType) { 1090 mapType = { value: 'enable' }; 1091 } 1092 1093 1094 // Initialize based on map type 1095 if (mapType.value === 'openstreetmap') { 1096 return mptbm_init_osm_map(); 1097 } else if (mapType.value === 'enable') { 1098 return mptbm_init_google_map(); 1099 } else { 1100 return false; 1101 } 1102} 1103 1104// Safety net called right before the first marker is placed: if the map 1105// somehow isn't up yet (e.g. its container was hidden when 1106// mptbm_map_area_init() ran), build it lazily now instead of silently 1107// dropping the marker. A no-op once the map already exists. 1108function mptbm_ensure_osm_map_ready() { 1109 if (mptbm_osm_map) { 1110 return true; 1111 } 1112 var mapContainer = mptbm_get_current_map_area(); 1113 if (!mapContainer) { 1114 return false; 1115 } 1116 mapContainer.innerHTML = ''; 1117 return mptbm_init_osm_map(); 1118} 1119 1120function mptbm_init_osm_map() { 1121 1122 if (typeof L === 'undefined') { 1123 return false; 1124 } 1125 1126 // Check if map container exists 1127 var mapContainer = mptbm_get_current_map_area(); 1128 if (!mapContainer) { 1129 return false; 1130 } 1131 1132 // Clean up existing map instance if it exists 1133 if (mptbm_osm_map) { 1134 try { 1135 mptbm_osm_map.remove(); 1136 mptbm_osm_map = null; 1137 mptbm_osm_markers = []; 1138 mptbm_osm_route = null; 1139 mptbm_osm_start_marker = null; 1140 mptbm_osm_end_marker = null; 1141 mptbm_osm_extra_marker = null; 1142 } catch (e) { 1143 console.log("[OSM] Error removing map:", e); 1144 } 1145 } 1146 1147 1148 // Get default coordinates from PHP or use fallback 1149 var defaultLat = (typeof mptbm_default_lat !== 'undefined') ? mptbm_default_lat : 40.7128; 1150 var defaultLng = (typeof mptbm_default_lng !== 'undefined') ? mptbm_default_lng : -74.0060; 1151 1152 // Initialize OpenStreetMap with configured coordinates. Passed as the 1153 // resolved element (not the 'mptbm_map_area' id string) since that id is 1154 // duplicated across tabs -- Leaflet would otherwise resolve it via its 1155 // own getElementById and land on the wrong tab's div. 1156 mptbm_osm_map = L.map(mapContainer).setView([defaultLat, defaultLng], 10); 1157 1158 // Add OpenStreetMap tiles 1159 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { 1160 attribution: '© OpenStreetMap contributors' 1161 }).addTo(mptbm_osm_map); 1162 1163 // Initialize address search functionality 1164 mptbm_init_osm_address_search(); 1165 1166 mptbm_render_manual_osm_locations(); 1167 1168 return true; 1169} 1170 1171function mptbm_init_osm_address_search() { 1172 1173 // Clean up any existing autocomplete containers 1174 var existingContainers = document.querySelectorAll('.mptbm-osm-autocomplete'); 1175 existingContainers.forEach(function (container) { 1176 container.remove(); 1177 }); 1178 1179 var startInput = document.getElementById('mptbm_map_start_place'); 1180 var endInput = document.getElementById('mptbm_map_end_place'); 1181 1182 1183 if (startInput) { 1184 mptbm_setup_osm_autocomplete(startInput, 'start'); 1185 } 1186 if (endInput) { 1187 mptbm_setup_osm_autocomplete(endInput, 'end'); 1188 } 1189 var extraInput = document.getElementById('mptbm_map_extra_stop_place'); 1190 if (extraInput) { 1191 mptbm_setup_osm_autocomplete(extraInput, 'extra'); 1192 } 1193} 1194 1195function mptbm_cleanup_osm_autocomplete(input) { 1196 if (!input || !input._mptbmOsmAutocomplete) { 1197 return; 1198 } 1199 1200 var state = input._mptbmOsmAutocomplete; 1201 1202 if (state.debounceTimer) { 1203 clearTimeout(state.debounceTimer); 1204 } 1205 1206 if (state.abortController) { 1207 state.abortController.abort(); 1208 } 1209 1210 if (state.handlers) { 1211 input.removeEventListener('input', state.handlers.input); 1212 window.removeEventListener('scroll', state.handlers.positionDropdown); 1213 window.removeEventListener('resize', state.handlers.positionDropdown); 1214 document.removeEventListener('click', state.handlers.documentClick); 1215 } 1216 1217 if (state.container && state.container.parentNode) { 1218 state.container.parentNode.removeChild(state.container); 1219 } 1220 1221 delete input._mptbmOsmAutocomplete; 1222 input.removeAttribute('data-osm-autocomplete-initialized'); 1223} 1224 1225function mptbm_setup_osm_autocomplete(input, type) { 1226 mptbm_cleanup_osm_autocomplete(input); 1227 1228 var autocompleteState = { 1229 abortController: null, 1230 container: null, 1231 currentSearchQuery: '', 1232 debounceTimer: null, 1233 handlers: null 1234 }; 1235 var resultsContainer = document.createElement('div'); 1236 resultsContainer.className = 'mptbm-osm-autocomplete'; 1237 resultsContainer.setAttribute('data-autocomplete-type', type); 1238 resultsContainer.style.cssText = 'position: fixed; box-sizing: border-box; font-size:14px; background: #fff; border: 1px solid #e7eaf0; border-radius: 14px; max-height: 240px; overflow-y: auto; z-index: 99999 !important; display: none; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06); padding: 6px;'; 1239 1240 // Append to body to avoid parent overflow issues 1241 document.body.appendChild(resultsContainer); 1242 autocompleteState.container = resultsContainer; 1243 input._mptbmOsmAutocomplete = autocompleteState; 1244 1245 // Mark input as initialized 1246 input.setAttribute('data-osm-autocomplete-initialized', 'true'); 1247 1248 // Function to position the dropdown 1249 function positionDropdown() { 1250 var rect = input.getBoundingClientRect(); 1251 // For fixed positioning, don't add scroll offset - use viewport coordinates directly 1252 var top = rect.bottom + 2; 1253 var left = rect.left; 1254 var width = rect.width; 1255 1256 resultsContainer.style.top = top + 'px'; 1257 resultsContainer.style.left = left + 'px'; 1258 resultsContainer.style.width = width + 'px'; 1259 1260 } 1261 1262 function handleInput(e) { 1263 clearTimeout(autocompleteState.debounceTimer); 1264 var query = e.target.value.trim(); 1265 1266 if (query.length < 3) { 1267 if (autocompleteState.abortController) { 1268 autocompleteState.abortController.abort(); 1269 autocompleteState.abortController = null; 1270 } 1271 resultsContainer.style.display = 'none'; 1272 autocompleteState.currentSearchQuery = ''; 1273 return; 1274 } 1275 1276 // Store the current query 1277 autocompleteState.currentSearchQuery = query; 1278 1279 autocompleteState.debounceTimer = setTimeout(function () { 1280 positionDropdown(); 1281 mptbm_search_osm_address(query, resultsContainer, input, type, autocompleteState.currentSearchQuery, autocompleteState); 1282 }, 300); 1283 } 1284 1285 input.addEventListener('input', handleInput); 1286 1287 // Reposition on scroll or resize 1288 window.addEventListener('scroll', positionDropdown); 1289 window.addEventListener('resize', positionDropdown); 1290 1291 // Hide results when clicking outside 1292 function handleDocumentClick(e) { 1293 if (e.target !== input && !resultsContainer.contains(e.target)) { 1294 resultsContainer.style.display = 'none'; 1295 } 1296 } 1297 1298 document.addEventListener('click', handleDocumentClick); 1299 1300 autocompleteState.handlers = { 1301 input: handleInput, 1302 positionDropdown: positionDropdown, 1303 documentClick: handleDocumentClick 1304 }; 1305} 1306 1307// Bangladesh-specific fallback matching, ported from the old server-side
1308// proxy (MPTBM_Dependencies::osm_search_proxy) -- Photon's country field is 1309// sometimes empty for BD results, so city/state name also counts as a match. 1310var MPTBM_BD_CITIES = ['DHAKA', 'CHITTAGONG', 'SYLHET', 'RAJSHAHI', 'KHULNA', 'BARISAL', 'RANGPUR', 'COMILLA', 'NARAYANGANJ', 'GAZIPUR']; 1311var MPTBM_BD_STATES = ['DHAKA', 'CHITTAGONG', 'SYLHET', 'RAJSHAHI', 'KHULNA', 'BARISAL', 'RANGPUR', 'DIVISION']; 1312 1313// Same GeoJSON -> {display_name, lat, lon} shape and country filtering the 1314// PHP proxy used to do server-side, ported 1:1 so behavior is unchanged now 1315// that the call happens directly from the browser. 1316function mptbm_transform_photon_results(geojson, restrictToCountry, countryCode) { 1317 var results = []; 1318 if (!geojson || !Array.isArray(geojson.features)) { 1319 return results; 1320 } 1321 1322 geojson.features.forEach(function (feature) { 1323 var props = feature.properties || {}; 1324 var coords = (feature.geometry && feature.geometry.coordinates) || []; 1325 1326 if (restrictToCountry && countryCode) { 1327 var featureCountry = (props.countrycode || '').toUpperCase(); 1328 var featureCountryName = (props.country || '').toUpperCase(); 1329 var featureState = (props.state || '').toUpperCase(); 1330 var featureCity = (props.city || '').toUpperCase(); 1331 var matches = false; 1332 1333 if (featureCountry && featureCountry === countryCode) { 1334 matches = true; 1335 } 1336 1337 if (!matches && countryCode === 'BD') { 1338 if (featureCountryName === 'BANGLADESH' || featureCountryName === 'BD') { 1339 matches = true; 1340 } 1341 if (!matches && MPTBM_BD_CITIES.indexOf(featureCity) !== -1) { 1342 matches = true; 1343 } else if (!matches && MPTBM_BD_STATES.indexOf(featureState) !== -1) { 1344 matches = true; 1345 } 1346 } 1347 1348 // No country info at all on this result -- allow it through 1349 // rather than silently dropping it. 1350 if (!featureCountry && !featureCountryName && !featureCity && !featureState) { 1351 matches = true; 1352 } 1353 1354 if (!matches) { 1355 return; 1356 } 1357 } 1358 1359 // Place name + city is enough to identify a pickup/drop-off for a 1360 // service operating in one city/country - state and country were 1361 // repeated on every single result (e.g. "..., Dhaka Division, 1362 // Bangladesh") and made addresses needlessly long throughout the 1363 // booking flow (summary, admin bookings list, order emails, ...). 1364 // Only fall back to the broader state/country if neither a specific 1365 // name nor a city came back at all, so a legitimately resolvable 1366 // rural result still shows something instead of "Unknown Location". 1367 var nameParts = [props.name, props.city].filter(Boolean); 1368 if (!nameParts.length) { 1369 nameParts = [props.state, props.country].filter(Boolean); 1370 } 1371 1372 results.push({ 1373 display_name: nameParts.length ? nameParts.join(', ') : 'Unknown Location', 1374 lat: coords.length > 1 ? coords[1] : 0, 1375 lon: coords.length > 0 ? coords[0] : 0 1376 }); 1377 }); 1378 1379 return results; 1380} 1381 1382function mptbm_render_osm_search_results(results, container, input, type) { 1383 container.innerHTML = ''; 1384 1385 if (!results || results.length === 0) { 1386 container.innerHTML = '<div style="padding: 9px 12px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #94a3b8; font-size: 13px; font-weight: 600;">No results found</div>'; 1387 container.style.display = 'block'; 1388 return; 1389 } 1390 1391 results.forEach(function (result) { 1392 var item = document.createElement('div'); 1393 item.style.cssText = 'display: flex; align-items: flex-start; gap: 10px; padding: 10px 12px; margin: 2px 0; cursor: pointer; border-radius: 10px; color: #0f172a; font-size: 13.5px; font-weight: 500; line-height: 1.4; transition: background-color .15s ease;'; 1394 1395 var icon = document.createElement('i'); 1396 icon.className = 'fas fa-map-marker-alt'; 1397 icon.style.cssText = 'flex: 0 0 auto; width: 14px; margin-top: 2px; color: #94a3b8; font-size: 13px;'; 1398 1399 var text = document.createElement('span'); 1400 text.textContent = result.display_name; 1401 text.style.cssText = 'flex: 1 1 auto; min-width: 0;'; 1402 1403 item.appendChild(icon); 1404 item.appendChild(text); 1405 1406 item.addEventListener('click', function () { 1407 input.value = result.display_name; 1408 container.style.display = 'none'; 1409 mptbm_handle_osm_address_selection(result, type); 1410 }); 1411 1412 item.addEventListener('mouseenter', function () { 1413 this.style.backgroundColor = '#f1f4f9'; 1414 }); 1415 1416 item.addEventListener('mouseleave', function () { 1417 this.style.backgroundColor = 'transparent'; 1418 }); 1419 1420 container.appendChild(item); 1421 }); 1422 1423 container.style.display = 'block'; 1424} 1425 1426// Calls Photon directly from the browser instead of proxying through 1427// admin-ajax.php. The proxy added a full WordPress bootstrap plus an 1428// outbound request from the server itself on every keystroke search -- 1429// on hosts where the server's own outbound connection is slow (the same 1430// class of issue as a cURL timeout to any other external API), that hop 1431// alone could make autocomplete take several seconds per query. Calling 1432// Photon straight from the visitor's browser removes that hop entirely; 1433// Photon's public API already supports CORS for exactly this use. 1434function mptbm_search_osm_address(query, container, input, type, expectedQuery, autocompleteState, retryCount) { 1435 retryCount = retryCount || 0; 1436 var cacheKey = query.toLowerCase(); 1437 var cached = mptbm_osm_search_cache[cacheKey]; 1438 if (cached) { 1439 mptbm_render_osm_search_results(cached, container, input, type); 1440 return; 1441 } 1442 1443 container.innerHTML = '<div style="padding: 9px 12px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #94a3b8; font-size: 13px; font-weight: 600;">Searching…</div>'; 1444 container.style.display = 'block'; 1445 1446 var restrictEl = document.getElementById('mptbm_restrict_search_country'); 1447 var countryEl = document.getElementById('mptbm_country'); 1448 var restrictToCountry = !!restrictEl && restrictEl.value === 'yes'; 1449 var countryCode = (countryEl && countryEl.value ? countryEl.value : '').toUpperCase(); 1450 1451 var abortController = null; 1452 var timeoutId = null; 1453 if (autocompleteState && typeof AbortController !== 'undefined') { 1454 if (autocompleteState.abortController) { 1455 autocompleteState.abortController.abort(); 1456 } 1457 1458 abortController = new AbortController(); 1459 autocompleteState.abortController = abortController; 1460 // Photon is a shared public instance -- bound the wait instead of 1461 // leaving "Searching..." on screen indefinitely if it stalls. 1462 timeoutId = setTimeout(function () { 1463 abortController.abort(); 1464 }, 8000); 1465 } 1466 1467 var url = 'https://photon.komoot.io/api/?q=' + encodeURIComponent(query) + '&limit=5&lang=en'; 1468 1469 fetch(url, { 1470 signal: abortController ? abortController.signal : undefined 1471 }) 1472 .then(function (response) { 1473 return response.json(); 1474 }) 1475 .then(function (geojson) { 1476 if (timeoutId) { 1477 clearTimeout(timeoutId); 1478 } 1479 if (autocompleteState && autocompleteState.abortController === abortController) { 1480 autocompleteState.abortController = null; 1481 } 1482 1483 // Check if this response is still relevant (user hasn't typed more) 1484 var currentValue = input.value.trim(); 1485 if (expectedQuery && currentValue !== expectedQuery) { 1486 return; 1487 } 1488 1489 var results = mptbm_transform_photon_results(geojson, restrictToCountry, countryCode); 1490 mptbm_osm_search_cache[cacheKey] = results; 1491 mptbm_render_osm_search_results(results, container, input, type); 1492 }) 1493 .catch(function (error) { 1494 if (timeoutId) { 1495 clearTimeout(timeoutId); 1496 } 1497 if (autocompleteState && autocompleteState.abortController === abortController) { 1498 autocompleteState.abortController = null; 1499 } 1500
1501 if (error && error.name === 'AbortError') { 1502 return; 1503 } 1504 1505 // fetch() collapses every network-layer failure (DNS hiccup, a 1506 // VPN/Wi-Fi adapter flapping mid-request -- ERR_NETWORK_CHANGED, 1507 // a dropped connection, ...) into this same generic "Failed to 1508 // fetch" TypeError with no way to tell them apart. Most of those 1509 // are transient and gone a moment later, so retry once 1510 // automatically before bothering the visitor with an error -- 1511 // only if they haven't since typed something else. 1512 console.error('[OSM Search] Fetch error:', error); 1513 if (retryCount < 1 && input.value.trim() === expectedQuery) { 1514 setTimeout(function () { 1515 if (input.value.trim() === expectedQuery) { 1516 mptbm_search_osm_address(query, container, input, type, expectedQuery, autocompleteState, retryCount + 1); 1517 } 1518 }, 700); 1519 return; 1520 } 1521 1522 container.innerHTML = '<div style="padding: 9px 12px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; text-align: center; color: #dc2626; font-size: 13px; font-weight: 600;">Search failed. Please try again.</div>'; 1523 container.style.display = 'block'; 1524 }); 1525} 1526 1527function mptbm_handle_osm_address_selection(address, type) { 1528 var lat = parseFloat(address.lat); 1529 var lng = parseFloat(address.lon); 1530 var price_based = jQuery('[name="mptbm_price_based"]').val(); 1531 1532 // First address picked: swap the idle Google preview for the real map. 1533 mptbm_ensure_osm_map_ready(); 1534 1535 // Remove existing marker for this type 1536 if (type === 'start' && mptbm_osm_start_marker) { 1537 mptbm_osm_map.removeLayer(mptbm_osm_start_marker); 1538 } else if (type === 'end' && mptbm_osm_end_marker) { 1539 mptbm_osm_map.removeLayer(mptbm_osm_end_marker); 1540 } else if (type === 'extra' && mptbm_osm_extra_marker) { 1541 mptbm_osm_map.removeLayer(mptbm_osm_extra_marker); 1542 } 1543 1544 // Create new marker if map exists 1545 if (mptbm_osm_map) { 1546 var marker = L.marker([lat, lng]).addTo(mptbm_osm_map); 1547 marker.bindPopup(address.display_name); 1548 1549 if (type === 'start') { 1550 mptbm_osm_start_marker = marker; 1551 window.mptbm_fixed_zone_start_coords = { latitude: lat, longitude: lng }; 1552 // Remembers exactly which address text these coordinates belong 1553 // to, so the Search button can reuse them instead of re-geocoding 1554 // the same text again -- see getCachedOrFreshCoordinates(). 1555 window.mptbm_osm_start_coords_address = address.display_name; 1556 } else if (type === 'end') { 1557 mptbm_osm_end_marker = marker; 1558 window.mptbm_fixed_zone_end_coords = { latitude: lat, longitude: lng }; 1559 window.mptbm_osm_end_coords_address = address.display_name; 1560 } else if (type === 'extra') { 1561 mptbm_osm_extra_marker = marker; 1562 } 1563 1564 // Calculate distance if we have start marker and either end marker OR extra marker 1565 if (mptbm_osm_start_marker && (mptbm_osm_end_marker || mptbm_osm_extra_marker)) { 1566 mptbm_calculate_osm_distance(); 1567 } 1568 1569 // Fit map to show all markers 1570 var markersToFit = [mptbm_osm_start_marker, mptbm_osm_end_marker]; 1571 if (mptbm_osm_extra_marker) markersToFit.push(mptbm_osm_extra_marker); 1572 1573 var group = new L.featureGroup(markersToFit.filter(Boolean)); 1574 if (group.getLayers().length > 0) { 1575 mptbm_osm_map.fitBounds(group.getBounds().pad(0.1)); 1576 } 1577 } 1578} 1579 1580// Warn the user when the selected "Select Hours" rental duration is shorter 1581// than the estimated drive time for the chosen pickup/drop-off route. 1582function mptbm_update_fixed_hours_warning() { 1583 var priceBasedEl = document.querySelector('[name="mptbm_price_based"]'); 1584 if (!priceBasedEl || priceBasedEl.value !== 'fixed_hourly') return; 1585 1586 var warningEl = document.getElementById('mptbm_fixed_hours_warning'); 1587 var hoursEl = document.getElementById('mptbm_fixed_hours'); 1588 var durationEl = document.getElementById('mptbm_calculated_duration'); 1589 if (!warningEl || !hoursEl || !durationEl) return; 1590 1591 var durationSeconds = parseFloat(durationEl.value) || 0; 1592 var selectedHours = parseInt(hoursEl.value, 10) || 0; 1593 1594 if (durationSeconds <= 0) { 1595 warningEl.style.display = 'none'; 1596 return; 1597 } 1598 1599 var requiredHours = Math.max(1, Math.ceil(durationSeconds / 3600)); 1600 1601 if (requiredHours > selectedHours) { 1602 var totalMinutes = Math.round(durationSeconds / 60); 1603 var h = Math.floor(totalMinutes / 60); 1604 var m = totalMinutes % 60; 1605 var tripTimeText = h > 0 ? (h + 'h' + (m > 0 ? ' ' + m + 'm' : '')) : m + 'm'; 1606 var hourWord = requiredHours === 1 ? 'hour' : 'hours'; 1607 1608 var textEl = warningEl.querySelector('span'); 1609 if (textEl) { 1610 textEl.textContent = 'Estimated trip time is ~' + tripTimeText + '. Select at least ' + requiredHours + ' ' + hourWord + ' to cover this trip.'; 1611 } 1612 warningEl.style.display = 'flex'; 1613 } else { 1614 warningEl.style.display = 'none'; 1615 } 1616} 1617 1618jQuery(document).on('mp_change change', '#mptbm_fixed_hours', function () { 1619 mptbm_update_fixed_hours_warning(); 1620}); 1621 1622// Custom-styled dropdown proxy for Transfer Type / Extra Waiting Hours. 1623// A real (visually hidden) <select> stays in the DOM so existing behaviour 1624// (the Return-date/time collapse toggle, price refresh listeners) keeps 1625// working untouched -- clicking a list item just updates that select and 1626// fires a native "change" event on it. 1627(function ($) { 1628 "use strict"; 1629 1630 $(document).on('click', '.mptbm_select_proxy input.formControl', function (e) { 1631 e.stopPropagation(); 1632 var $list = $(this).closest('.mptbm_select_proxy').find('.mp_input_select_list'); 1633 var isOpen = $list.is(':visible'); 1634 $('.mptbm_select_proxy .mp_input_select_list').not($list).slide
1634Up(200); 1635 $list[isOpen ? 'slideUp' : 'slideDown'](200); 1636 }); 1637 1638 $(document).on('click', '.mptbm_select_proxy .mp_input_select_list li', function (e) { 1639 e.preventDefault(); 1640 var $li = $(this); 1641 var $wrapper = $li.closest('.mptbm_select_proxy'); 1642 1643 $wrapper.find('input.formControl').val($li.text()); 1644 $wrapper.find('select.mptbm_proxy_native_select').val($li.data('value')).trigger('change'); 1645 $wrapper.find('.mp_input_select_list').slideUp(200); 1646 }); 1647 1648 $(document).on('click', function (e) { 1649 if (!$(e.target).closest('.mptbm_select_proxy').length) { 1650 $('.mptbm_select_proxy .mp_input_select_list').slideUp(200); 1651 } 1652 }); 1653})(jQuery); 1654 1655function mptbm_calculate_osm_distance() { 1656 // Multi-row "Add Extra Stop" coordinates (get_details.php's 1657 // .mptbm_extra_stops_wrapper), read fresh in DOM order so added/edited/ 1658 // removed rows are always reflected. OSRM needs actual coordinates 1659 // (unlike Google's Directions API, which can geocode address text 1660 // itself), which is exactly what the paired .mptbm_extra_stop_coords 1661 // hidden field stores ("lat,lng") once a suggestion is picked. 1662 var multiStopCoords = []; 1663 jQuery('.mptbm_extra_stop_coords').each(function () { 1664 var val = jQuery(this).val(); 1665 if (!val) return; 1666 var parts = val.split(','); 1667 if (parts.length === 2) { 1668 var lat = parseFloat(parts[0]); 1669 var lng = parseFloat(parts[1]); 1670 if (!isNaN(lat) && !isNaN(lng)) { 1671 multiStopCoords.push({ lat: lat, lng: lng }); 1672 } 1673 } 1674 }); 1675 1676 // We need at least start marker and either end marker, extra marker, or a multi-stop 1677 if (!mptbm_osm_start_marker || (!mptbm_osm_end_marker && !mptbm_osm_extra_marker && !multiStopCoords.length)) return; 1678 1679 var startLatLng = mptbm_osm_start_marker.getLatLng(); 1680 1681 // Use end marker if available; otherwise fall back to the last known 1682 // stop (a multi-row stop if any exist, else the legacy single extra 1683 // marker) as a temporary destination so a route still draws. 1684 var endLatLng; 1685 if (mptbm_osm_end_marker) { 1686 endLatLng = mptbm_osm_end_marker.getLatLng(); 1687 } else if (multiStopCoords.length) { 1688 var lastStop = multiStopCoords.pop(); 1689 endLatLng = L.latLng(lastStop.lat, lastStop.lng); 1690 } else { 1691 endLatLng = mptbm_osm_extra_marker.getLatLng(); 1692 } 1693 1694 // Determine if we should use the legacy single extra marker as a 1695 // waypoint (only if it isn't itself being used as the destination above) 1696 var useExtraAsWaypoint = (mptbm_osm_end_marker || multiStopCoords.length) && mptbm_osm_extra_marker; 1697 1698 var urlCoords = startLatLng.lng + ',' + startLatLng.lat; 1699 1700 if (useExtraAsWaypoint) { 1701 var extraLatLng = mptbm_osm_extra_marker.getLatLng(); 1702 urlCoords += ';' + extraLatLng.lng + ',' + extraLatLng.lat; 1703 } 1704 1705 multiStopCoords.forEach(function (coord) { 1706 urlCoords += ';
1706' + coord.lng + ',' + coord.lat; 1707 }); 1708 1709 urlCoords += ';' + endLatLng.lng + ',' + endLatLng.lat; 1710 1711 // Get route from OSRM (Open Source Routing Machine) 1712 var osrmUrl = 'https://router.project-osrm.org/route/v1/driving/' + 1713 urlCoords + 1714 '?overview=full&geometries=geojson'; 1715 1716 fetch(osrmUrl) 1717 .then(response => response.json()) 1718 .then(data => { 1719 1720 if (data.code === 'Ok' && data.routes && data.routes.length > 0) { 1721 var route = data.routes[0]; 1722 var distanceInMeters = route.distance; // Distance in meters 1723 var durationInSeconds = route.duration; // Duration in seconds 1724 var distance = distanceInMeters / 1000; // Convert meters to km 1725 var duration = durationInSeconds / 3600; // Convert seconds to hours 1726 1727 1728 // Prepare cookie data 1729 var kmOrMile = document.getElementById('mptbm_km_or_mile').value; 1730 var distance_text, display_distance; 1731 1732 if (kmOrMile === 'mile') { 1733 // Convert to miles 1734 var distanceInMiles = distance * 0.621371; 1735 distance_text = distanceInMiles.toFixed(1) + ' miles'; 1736 display_distance = ' ' + distanceInMiles.toFixed(1) + ' MILE'; 1737 } else { 1738 distance_text = distance.toFixed(1) + ' km'; 1739 display_distance = ' ' + distance.toFixed(1) + ' KM'; 1740 } 1741 1742 // Format duration text 1743 var hours = Math.floor(duration); 1744 var minutes = Math.round((duration - hours) * 60); 1745 var duration_text; 1746 if (hours > 0) { 1747 duration_text = hours + ' Hour ' + minutes + ' Min'; 1748 } else { 1749 duration_text = minutes + ' Min'; 1750 } 1751 1752 // Set cookies for price calculation (same format as Google Maps) 1753 var now = new Date(); 1754 now.setTime(now.getTime() + (24 * 60 * 60 * 1000)); // 24 hours 1755 var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax"; 1756 1757 document.cookie = "mptbm_distance=" + encodeURIComponent(distanceInMeters) + cookieOptions; 1758 document.cookie = "mptbm_distance_text=" + encodeURIComponent(distance_text) + cookieOptions; 1759 document.cookie = "mptbm_duration=" + encodeURIComponent(durationInSeconds) + cookieOptions; 1760 document.cookie = "mptbm_duration_text=" + encodeURIComponent(duration_text) + cookieOptions; 1761 1762 // Update explicit hidden fields 1763 var explicitDistance = document.getElementById('mptbm_calculated_distance'); 1764 if (explicitDistance) { 1765 explicitDistance.value = distanceInMeters; 1766 } 1767 var explicitDuration = document.getElementById('mptbm_calculated_duration'); 1768 if (explicitDuration) { 1769 explicitDuration.value = durationInSeconds; 1770 } 1771 1772 1773 // Update distance display 1774 var currentMapWrap = mptbm_get_current_map_wrap(); 1775 var distanceElement = currentMapWrap ? currentMapWrap.querySelector('.mptbm_total_distance') : null; 1776 if (distanceElement) { 1777 distanceElement.textContent = display_distance; 1778 } 1779 1780 // Update time display 1781 var timeElement = currentMapWrap ? currentMapWrap.querySelector('.mptbm_total_time') : null; 1782 if (timeElement) { 1783 timeElement.textContent = duration_text; 1784 } 1785 1786 // Show distance/time section 1787 jQuery(currentMapWrap).find('.mptbm_distance_time').slideDown('fast'); 1788 mptbm_update_fixed_hours_warning(); 1789 1790 // The numbers above came from OSRM, called straight from this browser. 1791 // The fare, however, is measured on the server - which may be configured 1792 // to use a different routing service entirely (Map API Settings > Routing 1793 // Service), because OSRM can only route over roads present in 1794 // OpenStreetMap and detours around the ones that are missing. Whenever 1795 // the two disagree the customer would read one distance here and be 1796 // charged for another, so ask the server what it measured and let that 1797 // win. The polyline below still comes from OSRM: it is only the drawn 1798 // shape, and the figures are what the fare is built on. 1799 mptbm_sync_distance_from_server(startLatLng, endLatLng); 1800 1801 // Draw route on map 1802 if (mptbm_osm_route) { 1803 mptbm_osm_map.removeLayer(mptbm_osm_route); 1804 } 1805 1806 // Convert GeoJSON coordinates to Leaflet format [lat, lng] 1807 var coordinates = route.geometry.coordinates.map(function (coord) { 1808 return [coord[1], coord[0]]; // GeoJSON uses [lng, lat], Leaflet uses [lat, lng] 1809 }); 1810 1811 var isManualRoute = currentMapWrap && currentMapWrap.getAttribute('data-manual-map') === 'yes'; 1812 mptbm_osm_route = L.polyline(coordinates, { 1813 color: isManualRoute ? '#059669' : '#ff4757', 1814 weight: isManualRoute ? 6 : 4, 1815 opacity: 0.9 1816 }).addTo(mptbm_osm_map); 1817 1818 // Fit map to show the entire route 1819 mptbm_osm_map.fitBounds(mptbm_osm_route.getBounds().pad(0.1)); 1820 1821 } else { 1822 console.error('[OSM Route] No route found'); 1823 // Fallback to straight line 1824 drawStraightLine(startLatLng, endLatLng); 1825 } 1826 }) 1827 .catch(error => { 1828 console.error('[OSM Route] Error fetching route:', error); 1829 // Fallback to straight line 1830 drawStraightLine(startLatLng, endLatLng); 1831 }); 1832 1833 // Fallback function to draw straight line 1834 function drawStraightLine(start, end) { 1835 var distance = mptbm_osm_map.distance(start, end) / 1000; 1836 1837 var currentMapWrap = mptbm_get_current_map_wrap(); 1838 var distanceElement = currentMapWrap ? currentMapWrap.querySelector('.mptbm_total_distance') : null; 1839 if (distanceElement) { 1840 var kmOrMile = document.getElementById('mptbm_km_or_mile').value; 1841 if (kmOrMile === 'mile') { 1842 distance = distance * 0.621371; 1843 distanceElement.textContent = ' ' + distance.toFixed(1) + ' MILE'; 1844 } else { 1845 distanceElement.textContent = ' ' + distance.toFixed(1) + ' KM'; 1846 } 1847 } 1848 1849 if (mptbm_osm_route) { 1850 mptbm_osm_map.removeLayer(mptbm_osm_route); 1851 } 1852 1853 mptbm_osm_route = L.polyline([start, end], { 1854 color: '#ff4757', 1855 weight: 4, 1856 opacity: 0.8, 1857 dashArray: '10, 10' // Dashed to show it's straight line 1858 }).addTo(mptbm_osm_map); 1859 } 1860} 1861 1862function mptbm_calculate_google_route_from_markers() { 1863 if (!mptbm_start_marker || !mptbm_end_marker || !mptbm_map) return; 1864 1865 var directionsService = new google.maps.DirectionsService(); 1866 var directionsRenderer = new google.maps.DirectionsRenderer(); 1867 directionsRenderer.setMap(mptbm_map); 1868 1869 var useShortestRoute = mptbm_use_shortest_route(); 1870 var request = { 1871 origin: mptbm_start_marker.getPosition(), 1872 destination: mptbm_end_marker.getPosition(), 1873 travelMode: google.maps.TravelMode.DRIVING, 1874 unitSystem: google.maps.UnitSystem.METRIC, 1875 }; 1876 if (useShortestRoute) { 1877 request.provideRouteAlternatives = true; 1878 } 1879 1880 directionsService.route(request, function (result, status) { 1881 if (status === google.maps.DirectionsStatus.OK) { 1882 try { 1883 // Same route the server will price when useShortestRoute is on - see 1884 // mptbm_shortest_route_index() above. 1885 var routeIndex = useShortestRoute ? mptbm_shortest_route_index(result.routes) : 0; 1886 var distance = result.routes[routeIndex].legs[0].distance.value; 1887 var kmOrMileElement = document.getElementById("mptbm_km_or_mile"); 1888 var kmOrMile = kmOrMileElement ? kmOrMileElement.value : 'km'; 1889 var distance_text = result.routes[routeIndex].legs[0].distance.text; 1890 var duration = result.routes[routeIndex].legs[0].duration.value; 1891 var duration_text = result.routes[routeIndex].legs[0].duration.text; 1892 1893 if (kmOrMile == 'mile') { 1894 var distanceInKilometers = distance / 1000; 1895 var distanceInMiles = distanceInKilometers * 0.621371; 1896 distance_text = distanceInMiles.toFixed(1) + ' miles'; 1897 } 1898 1899 var now = new Date(); 1900 var time = now.getTime(); 1901 var expireTime = time + 3600 * 1000 * 12; 1902 now.setTime(expireTime); 1903 var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax"; 1904 document.cookie = "mptbm_distance=" + encodeURIComponent(distance) + cookieOptions; 1905 document.cookie = "mptbm_distance_text=" + encodeURIComponent(distance_text) + cookieOptions; 1906 document.cookie = "mptbm_duration=" + encodeURIComponent(duration) + cookieOptions; 1907 document.cookie = "mptbm_duration_text=" + encodeURIComponent(duration_text) + cookieOptions; 1908 1909 var mapArea = jQuery('#mptbm_map_area').closest('.mptbm_transport_search_area'); 1910 if (mapArea.length > 0) { 1911 if (mapArea.find('input[name="mptbm_hidden_distance"]').length === 0) { 1912 mapArea.append('<input type="hidden" name="mptbm_hidden_distance" value="" />'); 1913 } 1914 if (mapArea.find('input[name="mptbm_hidden_duration"]').length === 0) { 1915 mapArea.append('<input type="hidden" name="mptbm_hidden_duration" value="" />'); 1916 } 1917 1918 var explicitDistance = document.getElementById('mptbm_calculated_distance'); 1919 if (explicitDistance) { 1920 explicitDistance.value = distance; 1921 } 1922 var explicitDuration = document.getElementById('mptbm_calculated_duration'); 1923 if (explicitDuration) { 1924 explicitDuration.value = duration; 1925 } 1926 1927 if (mapArea.find('input[name="mptbm_hidden_distance_text"]').length === 0) { 1928 mapArea.append('<input type="hidden" name="mptbm_hidden_distance_text" value="" />'); 1929 } 1930 if (mapArea.find('input[name="mptbm_hidden_duration_text"]').length === 0) { 1931 mapArea.append('<input type="hidden" name="mptbm_hidden_duration_text" value="" />'); 1932 } 1933 1934 mapArea.find('input[name="mptbm_hidden_distance"]').val(distance); 1935 mapArea.find('input[name="mptbm_hidden_duration"]').val(duration); 1936 mapArea.find('input[name="mptbm_hidden_distance_text"]').val(distance_text); 1937 mapArea.find('input[name="mptbm_hidden_duration_text"]').val(duration_text); 1938 } 1939 1940 directionsRenderer.setDirections(result); 1941 if (useShortestRoute) { 1942 directionsRenderer.setRouteIndex(routeIndex); 1943 } 1944 1945 var currentMapWrap = mptbm_get_current_map_wrap(); 1946 jQuery(currentMapWrap).find('.mptbm_total_distance').html(distance_text); 1947 jQuery(currentMapWrap).find('.mptbm_total_time').html(duration_text); 1948 jQuery(currentMapWrap).find('.mptbm_distance_time').slideDown('fast'); 1949 mptbm_update_fixed_hours_warning(); 1950 1951 // Fit map to show the entire route 1952 var bounds = new google.maps.LatLngBounds();
1953 result.routes[0].legs.forEach(function (leg) { 1954 bounds.extend(leg.start_location); 1955 bounds.extend(leg.end_location); 1956 }); 1957 mptbm_map.fitBounds(bounds); 1958 } catch (error) { 1959 console.error('[Google Maps Route] Error:', error); 1960 } 1961 } else { 1962 console.error('[Google Maps Route] Status:', status); 1963 } 1964 }); 1965} 1966 1967/** 1968 * Replace the displayed distance/time with the server's own measurement. 1969 * 1970 * Used by the OpenStreetMap path, where the browser routes via OSRM but the fare is 1971 * measured server-side through whichever routing service is configured. Silently 1972 * does nothing on failure, leaving the browser's figures in place - a route bar is 1973 * not worth an error message, and the search itself re-measures server-side anyway. 1974 */ 1975function mptbm_sync_distance_from_server(startLatLng, endLatLng) { 1976 if (typeof mp_ajax_url === 'undefined' || typeof mptbm_ajax === 'undefined' || !mptbm_ajax.search_nonce) { 1977 return; 1978 } 1979 jQuery.ajax({ 1980 type: 'POST', 1981 url: mp_ajax_url, 1982 data: { 1983 action: 'get_mptbm_route_distance', 1984 nonce: mptbm_ajax.search_nonce, 1985 start_place_coordinates: JSON.stringify({ latitude: startLatLng.lat, longitude: startLatLng.lng }), 1986 end_place_coordinates: JSON.stringify({ latitude: endLatLng.lat, longitude: endLatLng.lng }) 1987 }, 1988 success: function (response) { 1989 if (!response || !response.success || !response.data || !response.data.distance) { 1990 return; 1991 } 1992 var d = response.data; 1993 var currentMapWrap = mptbm_get_current_map_wrap(); 1994 var currentSearchArea = jQuery(currentMapWrap).closest('.mptbm_transport_search_area'); 1995 jQuery(currentMapWrap).find('.mptbm_total_distance').text(d.distance_text); 1996 jQuery(currentMapWrap).find('.mptbm_total_time').text(d.duration_text); 1997 1998 // Keep every carrier of the distance in step with what is now on screen - 1999 // the cookies the summary panel reads, the hidden fields the search posts, 2000 // and the explicit inputs the add-to-cart flow prefers. Leaving any of them 2001 // holding OSRM's figure would just reintroduce the mismatch one layer down. 2002 var expires = new Date(); 2003 expires.setTime(expires.getTime() + 12 * 3600 * 1000); 2004 var cookieOptions = '; expires=' + expires.toUTCString() + '; path=/; SameSite=Lax'; 2005 document.cookie = 'mptbm_distance=' + encodeURIComponent(d.distance) + cookieOptions; 2006 document.cookie = 'mptbm_distance_text=' + encodeURIComponent(d.distance_text) + cookieOptions; 2007 document.cookie = 'mptbm_duration=' + encodeURIComponent(d.duration) + cookieOptions; 2008 document.cookie = 'mptbm_duration_text=' + encodeURIComponent(d.duration_text) + cookieOptions; 2009 2010 var mapArea = currentSearchArea; 2011 mapArea.find('input[name="mptbm_hidden_distance"]').val(d.distance); 2012 mapArea.find('input[name="mptbm_hidden_duration"]').val(d.duration); 2013 mapArea.find('input[name="mptbm_hidden_distance_text"]').val(d.distance_text); 2014 mapArea.find('input[name="mptbm_hidden_duration_text"]').val(d.duration_text); 2015 2016 var explicitDistance = document.getElementById('mptbm_calculated_distance'); 2017 if (explicitDistance) { 2018 explicitDistance.value = d.distance; 2019 } 2020 var explicitDuration = document.getElementById('mptbm_calculated_duration'); 2021 if (explicitDuration) { 2022 explicitDuration.value = d.duration; 2023 } 2024 } 2025 }); 2026} 2027 2028function mptbm_init_google_map() { 2029 2030 // Check if Google Maps API is loaded 2031 if (typeof google === 'undefined' || typeof google.maps === 'undefined') { 2032 console.warn("[Google Map] Google Maps API not loaded. Skipping map initialization."); 2033 return false; 2034 } 2035 2036 2037 mptbm_set_cookie_distance_duration(); 2038 2039 // Initialize Google Places autocomplete for pickup location 2040 if (jQuery("#mptbm_map_start_place").length > 0) { 2041 var start_place = document.getElementById("mptbm_map_start_place"); 2042 var start_place_autoload = new google.maps.places.Autocomplete(start_place); 2043 var mptbm_restrict_search_to_country = jQuery('[name="mptbm_restrict_search_country"]').val(); 2044 var mptbm_country = jQuery('[name="mptbm_country"]').val(); 2045 2046 if (mptbm_restrict_search_to_country == 'yes') { 2047 start_place_autoload.setComponentRestrictions({ 2048 country: [mptbm_country] 2049 }); 2050 } 2051 2052 google.maps.event.addListener(start_place_autoload, "place_changed", function () { 2053 var end_place = document.getElementById("mptbm_map_end_place"); 2054 var price_based = jQuery('[name="mptbm_price_based"]').val(); 2055 2056 // Disable auto-syncing dropoff with pickup unless dropoff is hidden 2057 if (end_place && end_place.type === 'hidden') { 2058 end_place.value = start_place.value; 2059 } 2060 2061 // For fixed_zone_dropoff, set start marker when pickup is searched 2062 if (price_based === 'fixed_zone_dropoff' && start_place_autoload.getPlace()) { 2063 var place = start_place_autoload.getPlace(); 2064 if (place.geometry && place.geometry.location) { 2065 if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) { 2066 mptbm_start_marker.setMap(null); 2067 } 2068 mptbm_start_marker = new google.maps.Marker({ 2069 position: place.geometry.location, 2070 map: mptbm_map, 2071 title: place.name || place.formatted_address 2072 }); 2073 mptbm_map.setCenter(place.geometry.location); 2074 mptbm_map.setZoom(14); 2075 2076 // Calculate route if end marker exists 2077 if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) { 2078 mptbm_calculate_google_route_from_markers(); 2079 } 2080 } 2081 } 2082 2083 // Focus on Extra Stop if it exists, otherwise Dropoff 2084 var extra_stop = document.getElementById("mptbm_map_extra_stop_place"); 2085 if (extra_stop && extra_stop.offsetParent !== null) { // Check if visible 2086 extra_stop.focus(); 2087 } else if (end_place && end_place.offsetParent !== null) { 2088 end_place.focus(); 2089 } 2090 2091 mptbm_set_cookie_distance_duration( 2092 start_place.value, 2093 end_place ? end_place.value : start_place.value 2094 ); 2095 }); 2096 2097 // Mark as initialized to prevent duplicate initialization 2098 start_place.setAttribute('data-autocomplete-initialized', 'true'); 2099 } 2100 2101 // Ensure Next button is properly positioned after map initialization 2102 setTimeout(function () { 2103 var nextButtonContainer = document.querySelector('.get_details_next_link'); 2104 if (nextButtonContainer) { 2105 // Force a reflow to ensure proper positioning 2106 nextButtonContainer.style.display = 'none'; 2107 nextButtonContainer.offsetHeight; // Force reflow 2108 nextButtonContainer.style.display = ''; 2109 2110 // Ensure it's positioned correctly relative to the map 2111 var mapArea = document.querySelector('.mptbm_map_area'); 2112 if (mapArea && mapArea.style.display !== 'none') {
2113 nextButtonContainer.style.marginTop = '20px'; 2114 nextButtonContainer.style.position = 'relative'; 2115 nextButtonContainer.style.clear = 'both'; 2116 } 2117 } 2118 }, 100); 2119 2120 // Initialize Google Places autocomplete for dropoff location (only if it exists and is visible) 2121 if (jQuery("#mptbm_map_end_place").length > 0 && jQuery("#mptbm_map_end_place").is(":visible")) { 2122 var end_place = document.getElementById("mptbm_map_end_place"); 2123 var end_place_autoload = new google.maps.places.Autocomplete(end_place); 2124 var mptbm_restrict_search_to_country = jQuery('[name="mptbm_restrict_search_country"]').val(); 2125 var mptbm_country = jQuery('[name="mptbm_country"]').val(); 2126 2127 if (mptbm_restrict_search_to_country == 'yes') { 2128 end_place_autoload.setComponentRestrictions({ 2129 country: [mptbm_country] 2130 }); 2131 } 2132 2133 google.maps.event.addListener(end_place_autoload, "place_changed", function () { 2134 var start_place = document.getElementById("mptbm_map_start_place"); 2135 var price_based = jQuery('[name="mptbm_price_based"]').val(); 2136 2137 // For fixed_zone, set end marker when dropoff is searched 2138 if (price_based === 'fixed_zone' && end_place_autoload.getPlace()) { 2139 var place = end_place_autoload.getPlace(); 2140 if (place.geometry && place.geometry.location) { 2141 if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) { 2142 mptbm_end_marker.setMap(null); 2143 } 2144 mptbm_end_marker = new google.maps.Marker({ 2145 position: place.geometry.location, 2146 map: mptbm_map, 2147 title: place.name || place.formatted_address 2148 }); 2149 mptbm_map.setCenter(place.geometry.location); 2150 mptbm_map.setZoom(14); 2151 2152 // Calculate route if start marker exists 2153 if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) { 2154 mptbm_calculate_google_route_from_markers(); 2155 } 2156 } 2157 } 2158 2159 mptbm_set_cookie_distance_duration( 2160 start_place ? start_place.value : '', 2161 end_place ? end_place.value : '' 2162 ); 2163 }); 2164 } 2165 2166 mptbm_render_manual_google_locations(); 2167} 2168(function ($) { 2169 "use strict"; 2170 $(document).ready(function () { 2171 $(".mpStyle ul.mp_input_select_list").hide(); 2172 2173 // Function to initialize Google Places autocomplete (global scope) 2174 window.initializeGooglePlacesAutocomplete = function (retryCount = 0) { 2175 // Check if OpenStreetMap is being used instead 2176 var mapType = document.getElementById('mptbm_map_type'); 2177 2178 2179 if (mapType && mapType.value === 'openstreetmap') { 2180 return; 2181 } 2182 2183 2184 // Maximum retry attempts to prevent infinite loops 2185 const MAX_RETRIES = 10; 2186 const INITIAL_DELAY = 100; // Start with 100ms instead of 500ms 2187 2188 // Check if Google Maps API is loaded 2189 if (typeof google === 'undefined' || typeof google.maps === 'undefined' || typeof google.maps.places === 'undefined') { 2190 if (retryCount >= MAX_RETRIES) { 2191 console.warn('Google Maps API failed to load after', MAX_RETRIES, 'attempts. Please check your API key and connection.'); 2192 return; 2193 } 2194 2195 // Exponential backoff: 100ms, 200ms, 400ms, 800ms, etc. 2196 const delay = INITIAL_DELAY * Math.pow(2, retryCount); 2197 2198 setTimeout(function () { 2199 initializeGooglePlacesAutocomplete(retryCount + 1); 2200 }, delay); 2201 return; 2202 } 2203 2204 var startPlaceInput = document.getElementById('mptbm_map_start_place'); 2205 if (startPlaceInput && !startPlaceInput.hasAttribute('data-autocomplete-initialized')) { 2206 var startPlaceAutocomplete = new google.maps.places.Autocomplete(startPlaceInput); 2207 var mptbm_restrict_search_to_country = $('[name="mptbm_restrict_search_country"]').val(); 2208 var mptbm_country = $('[name="mptbm_country"]').val(); 2209 2210 if (mptbm_restrict_search_to_country == 'yes') { 2211 startPlaceAutocomplete.setComponentRestrictions({ 2212 country: [mptbm_country] 2213 }); 2214 } 2215 2216 google.maps.event.addListener(startPlaceAutocomplete, "place_changed", function () { 2217 var endPlaceInput = document.getElementById('mptbm_map_end_place'); 2218 var price_based = $('[name="mptbm_price_based"]').val(); 2219 var end_val = endPlaceInput ? endPlaceInput.value : (startPlaceInput ? startPlaceInput.value : ''); 2220 2221 if (price_based === 'fixed_zone_dropoff' && window.mptbm_fixed_zone_end_coords) { 2222 end_val = window.mptbm_fixed_zone_end_coords.latitude + "," + window.mptbm_fixed_zone_end_coords.longitude; 2223 } 2224 2225 // Only sync dropoff with pickup if dropoff is hidden (hourly pricing with disabled dropoff) 2226 if (endPlaceInput && endPlaceInput.type === 'hidden') { 2227 endPlaceInput.value = startPlaceInput.value; 2228 end_val = startPlaceInput.value; 2229 } 2230 2231 2232 2233 mptbm_set_cookie_distance_duration( 2234 startPlaceInput.value, 2235 end_val 2236 ); 2237 }); 2238 2239 // Mark as initialized to prevent duplicate initialization 2240 startPlaceInput.setAttribute('data-autocomplete-initialized', 'true'); 2241 } 2242 2243 // Initialize Google Places autocomplete for dropoff location (only if it exists and is visible) 2244 var endPlaceInput = document.getElementById('mptbm_map_end_place'); 2245 2246 2247 2248 2249 if (endPlaceInput && !endPlaceInput.hasAttribute('data-autocomplete-initialized') && endPlaceInput.type !== 'hidden') { 2250 var endPlaceAutocomplete = new google.maps.places.Autocomplete(endPlaceInput); 2251 var mptbm_restrict_search_to_country = $('[name="mptbm_restrict_search_country"]').val(); 2252 var mptbm_country = $('[name="mptbm_country"]').val(); 2253 2254 if (mptbm_restrict_search_to_country == 'yes') { 2255 endPlaceAutocomplete.setComponentRestrictions({ 2256 country: [mptbm_country] 2257 }); 2258 } 2259 2260 google.maps.event.addListener(endPlaceAutocomplete, 'place_changed', function () { 2261 var startInput = document.getElementById('mptbm_map_start_place'); 2262 var price_based = $('[name="mptbm_price_based"]').val(); 2263 var start_val = startInput ? startInput.value : ''; 2264 2265 if (price_based === 'fixed_zone' && window.mptbm_fixed_zone_start_coords) { 2266 start_val = window.mptbm_fixed_zone_start_coords.latitude + "," + window.mptbm_fixed_zone_start_coords.longitude; 2267 } 2268 2269 2270 2271 mptbm_set_cookie_distance_duration( 2272 start_val, 2273 endPlaceInput ? endPlaceInput.value : '' 2274 ); 2275 }); 2276 2277 endPlaceInput.setAttribute('data-autocomplete-initialized', 'true'); 2278 } 2279 2280 // Initialize Google Places autocomplete for EXTRA STOP location 2281 var extraStopInput = document.getElementById('mptbm_map_extra_stop_place'); 2282 2283 if (extraStopInput && !extraStopInput.hasAttribute('data-autocomplete-initialized')) { 2284 var extraStopAutocomplete = new google.maps.places.Autocomplete(extraStopInput); 2285 var mptbm_restrict_search_to_country = $('[name="mptbm_restrict_search_country"]').val(); 2286 var mptbm_country = $('[name="mptbm_country"]').val(); 2287 2288 if (mptbm_restrict_search_to_country == 'yes') { 2289 extraStopAutocomplete.setComponentRestrictions({ 2290 country: [mptbm_country] 2291 }); 2292 } 2293 2294 google.maps.event.addListener(extraStopAutocomplete, 'place_changed', function () { 2295 var place = extraStopAutocomplete.getPlace(); 2296 2297 // Create marker for extra stop 2298 if (place.geometry && place.geometry.location) { 2299 // Remove existing extra marker if present 2300 if (typeof mptbm_extra_marker !== 'undefined' && mptbm_extra_marker) { 2301 mptbm_extra_marker.setMap(null); 2302 } 2303 2304 // Create new marker for extra stop 2305 mptbm_extra_marker = new google.maps.Marker({ 2306 position: place.geometry.location, 2307 map: mptbm_map, 2308 title: place.name || place.formatted_address, 2309 label: { 2310 text: 'E', 2311 color: 'white', 2312 fontWeight: 'bold' 2313 } 2314 }); 2315 2316 mptbm_map.setCenter(place.geometry.location); 2317 mptbm_map.setZoom(14); 2318 } 2319 2320 // Update the map route when extra stop changes 2321 var startInput = document.getElementById('mptbm_map_start_place'); 2322 var endInput = document.getElementById('mptbm_map_end_place'); 2323 2324 mptbm_set_cookie_distance_duration( 2325 startInput ? startInput.value : '', 2326 endInput ? endInput.value : '' 2327 ); 2328 }); 2329 2330 extraStopInput.setAttribute('data-autocomplete-initialized', 'true'); 2331 } 2332 }; 2333 2334 // Maximum retry attempts to prevent infinite loops 2335 2336 // Initialize Google Places autocomplete on page load with a delay to ensure API is loaded 2337 setTimeout(function () { 2338 var mapType = document.getElementById('mptbm_map_type'); 2339 2340 if (mapType && mapType.value === 'openstreetmap') { 2341 mptbm_init_osm_address_search(); 2342 } else { 2343 initializeGooglePlacesAutocomplete(); 2344 } 2345 }, 100); // Reduced from 500ms to 100ms for faster initialization 2346 2347 // Handle Previous/Next button positioning after tab changes 2348 $(document).on('click', '.nextTab_prev, .nextTab_next', function () { 2349 setTimeout(function () { 2350 var nextButtonContainer = document.querySelector('.get_details_next_link'); 2351 if (nextButtonContainer) { 2352 // Force a reflow to ensure proper positioning 2353 nextButtonContainer.style.display = 'none'; 2354 nextButtonContainer.offsetHeight; // Force reflow 2355 nextButtonContainer.style.display = ''; 2356 2357 // Ensure it's positioned correctly relative to the map 2358 var mapArea = document.querySelector('.mptbm_map_area'); 2359 if (mapArea && mapArea.style.display !== 'none') { 2360 nextButtonContainer.style.marginTop = '20p
2360x'; 2361 nextButtonContainer.style.position = 'relative'; 2362 nextButtonContainer.style.clear = 'both'; 2363 } 2364 } 2365 }, 350); // Wait for slideDown animation to complete 2366 }); 2367 2368 // Function to validate and fix tab structure (silent version) 2369 function validateTabStructure() { 2370 // Check tab links 2371 $('.mptb-tabs li').each(function () { 2372 var tabId = $(this).attr('mptbm-data-tab'); 2373 var isCurrent = $(this).hasClass('current'); 2374 2375 // Check if corresponding tab content exists 2376 var tabContent = $("#" + tabId); 2377 if (tabContent.length === 0) { 2378 // Create missing tab content container 2379 var tabContainerParent = $('.mptb-tab-container'); 2380 if (tabContainerParent.length > 0) { 2381 var newTabContainer = $('<div id="' + tabId + '" class="mptb-tab-content"></div>'); 2382 tabContainerParent.append(newTabContainer); 2383 } 2384 } 2385 }); 2386 2387 // Check tab content containers 2388 $('.mptb-tab-content').each(function () { 2389 var tabId = $(this).attr('id'); 2390 var isCurrent = $(this).hasClass('current'); 2391 var isVisible = $(this).is(':visible'); 2392 2393 // Ensure current tab is visible 2394 if (isCurrent && !isVisible) { 2395 $(this).css('display', 'block'); 2396 } 2397 }); 2398 } 2399 2400 // Function to ensure loading spinner element exists 2401 window.ensureLoadingGifExists = function () { 2402 var loadingGif = $('.mptbm-hide-gif'); 2403 var tabContainer = $('.mptb-tab-container'); 2404 2405 if (loadingGif.length === 0 && tabContainer.length > 0) { 2406 var loadingSpinnerHtml = '<div class="mptbm-hide-gif mptbm-gif" style="display: none;"><div class="mptbm-spinner"></div></div>'; 2407 tabContainer.append(loadingSpinnerHtml); 2408 return true; 2409 } else if (loadingGif.length === 0 && tabContainer.length === 0) { 2410 return false; 2411 } 2412 return true; 2413 }; 2414 2415 // Try to create loading GIF element immediately 2416 window.ensureLoadingGifExists(); 2417 2418 // Also try after a short delay to ensure DOM is fully ready 2419 setTimeout(function () { 2420 window.ensureLoadingGifExists(); 2421 validateTabStructure(); 2422 }, 100); 2423 2424 // Only initialize map on page load if the first tab should have a map 2425 if ($("#mptbm_map_area").length > 0) { 2426 var hasTabs = $('.mptb-tabs').length > 0; 2427 if (hasTabs) { 2428 // Check if the current tab should have a map 2429 var currentTab = $('.mptb-tabs li.current').attr('mptbm-data-tab'); 2430 var mapEnabled = $('.mptb-tabs li.current').attr('mptbm-data-map'); 2431 2432 // Manual/flat-rate tabs may now have their own optional location map. 2433 if (mapEnabled === 'yes' && mptbm_get_current_map_wrap() && mptbm_get_current_map_wrap().style.display !== 'none') { 2434 mptbm_map_area_init(); 2435 } 2436 } else { 2437 // No tabs (plain [mptbm_booking]) â initialize map if container is visible 2438 var mapAreaEl = document.querySelector('.mptbm_map_area'); 2439 if (!mapAreaEl || mapAreaEl.style.display === 'none') { 2440 // Skip if hidden by template conditions 2441 } else { 2442 mptbm_map_area_init(); 2443 } 2444 } 2445 } 2446 }); 2447 2448 function mptbmNormalizeTimeToken(time) { 2449 var parts = String(time || '').replace(':', '.').split('.'); 2450 if (parts.length !== 2) { 2451 return ''; 2452 } 2453 2454 var hours = parseInt(parts[0], 10); 2455 var minutes = parseInt(parts[1], 10); 2456 if (isNaN(hours) || isNaN(minutes) || hours < 0 || hours > 23 || minutes < 0 || minutes > 59) { 2457 return ''; 2458 } 2459 2460 return String(hours).padStart(2, '0') + '.' + String(minutes).padStart(2, '0'); 2461 } 2462 2463 function mptbmRefreshVehicleTimeAvailability(parent) { 2464 var vehicleId = parseInt(parent.find('[name="mptbm_source_vehicle_id"]').val(), 10) || 0; 2465 var selectedDate = parent.find('#mptbm_map_start_date').val(); 2466 var timeItems = parent.find('.start_time_list li[data-time]'); 2467 2468 if (!selectedDate || !timeItems.length || typeof mp_ajax_url === 'undefined' || typeof mptbm_ajax === 'undefined') { 2469 return; 2470 } 2471 2472 var times = []; 2473 timeItems.each(function () { 2474 var time = mptbmNormalizeTimeToken($(this).attr('data-time')); 2475 if (time) { 2476 times.push(time); 2477 } 2478 }); 2479 2480 clearTimeout(parent.data('mptbm-time-availability-timer')); 2481 var previousRequest = parent.data('mptbm-time-availability-request'); 2482 if (previousRequest && previousRequest.readyState !== 4) { 2483 previousRequest.abort(); 2484 } 2485 2486 var timer = setTimeout(function () { 2487 var requestDate = selectedDate; 2488 var request = $.ajax({ 2489 type: 'POST', 2490 url: mp_ajax_url, 2491 data: { 2492 action: 'get_mptbm_vehicle_time_availability', 2493 nonce: mptbm_ajax.search_nonce, 2494 vehicle_id: vehicleId, 2495 date: requestDate, 2496 price_based: parent.find('[name="mptbm_price_based"]').val() || 'dynamic', 2497 times: times 2498 }, 2499 success: function (response) { 2500 if (!response.success || parent.find('#mptbm_map_start_date').val() !== requestDate) { 2501 return; 2502 } 2503 2504 var unavailable = response.data.unavailable_times || []; 2505 var unavailableLookup = {}; 2506 $.each(unavailable, function (index, time) { 2507 unavailableLookup[mptbmNormalizeTimeToken(time)] = true; 2508 }); 2509 2510 var unavailableLabel = response.data.unavailable_label || 'Booked'; 2511 var unavailableTitle = response.data.unavailable_title || unavailableLabel; 2512 var selectedTimeWasDisabled = false;
2513 2514 parent.find('.start_time_list li[data-time]').each(function () { 2515 var item = $(this); 2516 var isUnavailable = !!unavailableLookup[mptbmNormalizeTimeToken(item.attr('data-time'))]; 2517 2518 item.toggleClass('mptbm-time-unavailable', isUnavailable); 2519 if (isUnavailable) { 2520 item.attr({ 2521 'aria-disabled': 'true', 2522 'data-unavailable-label': unavailableLabel, 2523 'title': unavailableTitle 2524 }); 2525 if (String(parent.find('#mptbm_map_start_time').val()) === String(item.attr('data-value'))) { 2526 selectedTimeWasDisabled = true; 2527 } 2528 } else { 2529 item.removeAttr('aria-disabled data-unavailable-label title'); 2530 } 2531 }); 2532 2533 if (selectedTimeWasDisabled) { 2534 parent.find('#mptbm_map_start_time, #mptbm_start_time').val(''); 2535 } 2536 } 2537 }); 2538 2539 parent.data('mptbm-time-availability-request', request); 2540 }, 75); 2541 2542 parent.data('mptbm-time-availability-timer', timer); 2543 } 2544 2545 $(document).on('mptbm_time_options_updated', function () { 2546 $('.mptbm_transport_search_area').each(function () { 2547 mptbmRefreshVehicleTimeAvailability($(this)); 2548 }); 2549 }); 2550 2551 setTimeout(function () { 2552 $('.mptbm_transport_search_area').each(function () { 2553 mptbmRefreshVehicleTimeAvailability($(this)); 2554 }); 2555 }, 250); 2556 2557 $(document).on("click", "#mptbm_get_vehicle", function () { 2558 let searchButton = this; 2559 let parent = $(this).closest(".mptbm_transport_search_area"); 2560 let mptbm_enable_return_in_different_date = parent 2561 .find('[name="mptbm_enable_return_in_different_date"]') 2562 .val(); 2563 2564 let target = parent.find(".mptbm_inline_search_results"); 2565 let target_date = parent.find("#mptbm_map_start_date"); 2566 let return_target_date = parent.find("#mptbm_map_return_date"); 2567 let target_time = parent.find("#mptbm_map_start_time"); 2568 let return_target_time = parent.find("#mptbm_map_return_time"); 2569 let start_place; 2570 let end_place; 2571 let price_based = parent.find('[name="mptbm_price_based"]').val(); 2572 let two_way_field = parent.find('[name="mptbm_taxi_return"]'); 2573 let two_way = two_way_field.length ? two_way_field.val() : '1'; 2574 let waiting_time = parent.find('[name="mptbm_waiting_time"]').val(); 2575 let fixed_time = parent.find('[name="mptbm_fixed_hours"]').val(); 2576 let mptbm_original_price_base = parent.find('[name="mptbm_original_price_base"]').val(); 2577 let mptbm_source_vehicle_id = parseInt(parent.find('[name="mptbm_source_vehicle_id"]').val(), 10) || 0; 2578 2579 2580 let mptbm_enable_view_search_result_page = parent 2581 .find('[name="mptbm_enable_view_search_result_page"]') 2582 .val(); 2583 if (price_based === "manual") { 2584 start_place = document.getElementById("mptbm_manual_start_place"); 2585 end_place = document.getElementById("mptbm_manual_end_place"); 2586 } else if (price_based === "fixed_zone") { 2587 start_place = document.getElementById("mptbm_manual_start_place"); 2588 end_place = document.getElementById("mptbm_map_end_place"); 2589 } else if (price_based === "fixed_zone_dropoff") { 2590 start_place = document.getElementById("mptbm_map_start_place"); 2591 end_place = document.getElementById("mptbm_manual_end_place"); 2592 } else { 2593 start_place = document.getElementById("mptbm_map_start_place"); 2594 end_place = document.getElementById("mptbm_map_end_place"); 2595 } 2596 let start_date = target_date.val(); 2597 let return_date; 2598 let return_time; 2599 let has_return_fields = return_target_date.length > 0 && return_target_time.length > 0; 2600 2601 if (mptbm_enable_return_in_different_date == 'yes' && two_way != 1 && price_based != 'fixed_hourly' && has_return_fields) { 2602 return_date = return_target_date.val(); 2603 return_time = return_target_time.val(); 2604 2605 // Get the actual time from the data-time attribute (consistent with start_time) 2606 let selectedReturnTimeElement = parent.find("#mptbm_map_return_time").closest(".mp_input_select").find("li[data-value='" + return_time + "']"); 2607 if (selectedReturnTimeElement.length) { 2608 return_time = selectedReturnTimeElement.attr('data-time'); 2609 } 2610 2611 } else { 2612 return_date = start_date; 2613 return_time = 'Not applicable'; 2614 } 2615 let start_time = target_time.val(); 2616 // Get the actual time from the data-time attribute 2617 let selectedTimeElement = parent.find("#mptbm_map_start_time").closest(".mp_input_select").find("li[data-value='" + start_time + "']"); 2618 if (selectedTimeElement.length) { 2619 start_time = selectedTimeElement.attr('data-time'); 2620 2621 } 2622 2623 2624 2625 // Helper function to safely get value from input or select 2626 function getElementValue(element) { 2627 if (!element) return ''; 2628 if (element.tagName === 'SELECT') { 2629 return element.value || ''; 2630 } 2631 return element.value || ''; 2632 } 2633 2634 let start_place_value = getElementValue(start_place); 2635 let end_place_value = getElementValue(end_place); 2636 2637 if (!start_date) { 2638 target_date.trigger("click"); 2639 } else if (start_time === undefined || start_time === null || start_time === '') { 2640 parent 2641 .find("#mptbm_map_start_time") 2642 .closest(".mp_input_select") 2643 .find("input.formControl") 2644 .trigger("click"); 2645 } else if (!return_date) { 2646 if (mptbm_enable_return_in_different_date == 'yes' && two_way != 1 && has_return_fields) { 2647 return_target_date.trigger("click"); 2648 } 2649 } else if (return_time === undefined || return_time === null || return_time === '') { 2650 if (mptbm_enable_return_in_different_date == 'yes' && two_way != 1 && has_return_fields) { 2651 parent 2652 .find("#mptbm_map_return_time") 2653 .closest(".mp_input_select") 2654 .find("input.formControl") 2655 .trigger("click"); 2656 } 2657 } else if (!start_place_value || (start_place && start_place.tagName === 'SELECT' && start_place.options[start_place.selectedIndex] && start_place.options[start_place.selectedIndex].disabled)) { 2658 if (start_place) start_place.focus(); 2659 // Show error message 2660 let startMsg = price_based === 'manual' || price_based === 'fixed_zone' ? 'Please select a pickup location' : 'Please enter a pickup location'; 2661 if (start_place) showLocationError(start_place, startMsg); 2662 } else if (!end_place_value || (end_place && end_place.tagName === 'SELECT' && end_place.options[end_place.selectedIndex] && end_place.options[end_place.selectedIndex].disabled)) { 2663 // Check if dropoff is required (not hidden for hourly) 2664 let hideDropoff = parent.find('[name="mptbm_original_price_base"]').val() === 'fixed_hourly' && 2665 document.getElementById('mptbm_map_end_place').type === 'hidden'; 2666 if (!hideDropoff) { 2667 end_place.focus(); 2668 // Show error message 2669 let endMsg = price_based === 'manual' ? 'Please select a dropoff location' : 'Please enter a dropoff location'; 2670 showLocationError(end_place, endMsg); 2671 } 2672 } else { 2673 // Remove any existing error messages 2674 removeLocationErrors(); 2675 2676 var manualMapActive = price_based === 'manual' && parent.find('.mptbm_map_area').attr('data-manual-map') === 'yes'; 2677 var manualStartCoordinates = parent.data('mptbm-manual-start-coordinates'); 2678 var manualEndCoordinates = parent.data('mptbm-manual-end-coordinates'); 2679 if (manualMapActive && (!manualStartCoordinates || !manualEndCoordinates)) { 2680 mptbm_search_loading(parent, true); 2681 var startLabel = $(start_place).find('option:selected').data('label') || $(start_place).find('option:selected').text(); 2682 var endLabel = $(end_place).find('option:selected').data('label') || $(end_place).find('option:selected').text(); 2683 $.when( 2684 mptbm_resolve_manual_location(start_place_value, startLabel), 2685 mptbm_resolve_manual_location(end_place_value, endLabel) 2686 ).done(function (resolvedStart, resolvedEnd) { 2687 mptbm_search_loading(parent, false); 2688 if (!resolvedStart || !resolvedEnd) { 2689 showLocationError(end_place, 'Unable to locate this route on the map. Please try again.'); 2690 return; 2691 } 2692 parent.data('mptbm-manual-start-coordinates', resolvedStart); 2693 parent.data('mptbm-manual-end-coordinates', resolvedEnd); 2694 mptbm_update_manual_route_map(parent); 2695 $(searchButton).trigger('click'); 2696 }); 2697 return; 2698 } 2699 2700 mptbm_search_loading(parent, true); 2701 mptbm_content_refresh(parent); 2702 if (price_based !== "manual") { 2703 let calc_start = start_place_value; 2704 let calc_end = end_place_value; 2705 2706 if (price_based === 'fixed_zone' && window.mptbm_fixed_zone_start_coords) { 2707 calc_start = window.mptbm_fixed_zone_start_coords.latitude + "," + window.mptbm_fixed_zone_start_coords.longitude; 2708 } else if (price_based === 'fixed_zone_dropoff' && window.mptbm_fixed_zone_end_coords) { 2709 calc_end = window.mptbm_fixed_zone_end_coords.latitude + "," + window.mptbm_fixed_zone_end_coords.longitude; 2710 } 2711 2712 mptbm_set_cookie_distance_duration(calc_start, calc_end); 2713 } 2714 //let price_based = parent.find('[name="mptbm_price_based"]').val(); 2715 function getGeometryLocation(address, callback) { 2716 // Check if using OpenStreetMap 2717 var mapType = document.getElementById('mptbm_map_type'); 2718 if (mapType && mapType.value === 'openstreetmap') { 2719 // Use OpenStreetMap geocoding via our proxy 2720 var ajaxUrl = mptbm_ajax.ajax_url + '?action=mptbm_osm_search&nonce=' + mptbm_ajax.osm_nonce + '&q=' + encodeURIComponent(address); 2721 2722 fetch(ajaxUrl) 2723 .then(response => response.json()) 2724 .then(response => { 2725 if (response.success && response.data && response.data.length > 0) { 2726 var result = response.data[0]; 2727 var coordinatesOfPlace = { 2728 "latitude": parseFloat(result.lat), 2729 "longitude": parseFloat(result.lon) 2730 }; 2731 callback(coordinatesOfPlace); 2732 } else { 2733 console.error("OSM geocoding failed for:", address); 2734 callback(null); 2735 } 2736 }) 2737 .catch(error => { 2738 console.error("Error in OSM geocoding:", error); 2739 callback(null); 2740 }); 2741 } else { 2742 // Use Google Maps geocoding 2743 var geocoder = new google.maps.Geocoder(); 2744 var coordinatesOfPlace = {}; 2745 2746 geocoder.geocode({ address: address }, function (results, status) { 2747 2748 if (status === "OK") { 2749 try { 2750 var latitude = results[0].geometry.location.lat(); 2751 var longitude = results[0].geometry.location.lng(); 2752 coordinatesOfPlace["latitude"] = latitude; 2753 coordinatesOfPlace["longitude"] = longitude; 2754 // Call the callback function with the coordinates 2755 callback(coordinatesOfPlace); 2756 } catch (error) { 2757 console.error("Error processing geocoding results:", error); 2758 callback(null); 2759 } 2760 } else { 2761 console.error( 2762 "Geocode was not successful for the following reason: " + status 2763 ); 2764 // Call the callback function with null to indicate failure 2765 callback(null); 2766 } 2767 }); 2768 } 2769 } 2770 // Define a function to get the coordinates asynchronously and return a Deferred object 2771 2772 function getCoordinatesAsync(address) { 2773 var deferred = $.Deferred(); 2774 getGeometryLocation(address, function (coordinates) { 2775 deferred.resolve(coordinates); 2776 }); 2777 return deferred.promise(); 2778 } 2779 2780 // In OSM mode, picking a suggestion from the pickup/drop-off 2781 // autocomplete already geocoded this exact text once 2782 // (mptbm_handle_osm_address_selection stores the result + 2783 // the matched text). Re-geocoding the same address again here 2784 // was pure waste -- an extra round trip through the slower 2785 // server-side lookup, on every single Search click, for 2786 // something already known. Reuse it when the field's current 2787 // value still matches exactly what was picked; otherwise the 2788 // visitor edited the text since then, so fall back to a fresh 2789 // lookup rather than trust stale coordinates. 2790 function getCachedOrFreshCoordinates(address, cachedCoords, cachedAddress) { 2791 if (cachedCoords && cachedAddress === address) { 2792 return $.Deferred().resolve(cachedCoords).promise(); 2793 } 2794 return getCoordinatesAsync(address); 2795 } 2796 if (price_based !== 'manual') { 2797 2798 // For fixed_zone, pickup is from dropdown (term_XX), so we use pre-stored coords 2799 // Only geocode the end_place (dropoff search input) 2800 // For fixed_zone_dropoff, pickup is from map search, dropoff is from dropdown 2801 if (price_based === 'fixed_zone' || price_based === 'fixed_zone_dropoff') { 2802 let searchInput, dropdownCoords, startCoordinates, endCoordinates; 2803 2804 if (price_based === 'fixed_zone') { 2805 // fixed_zone: pickup = dropdown, dropoff = map search 2806 searchInput = end_place; 2807 dropdownCoords = window.mptbm_fixed_zone_start_coords || null; 2808 2809 let searchInputValue = getElementValue(searchInput); 2810 getCachedOrFreshCoordinates(searchInputValue, window.mptbm_fixed_zone_end_coords, window.mptbm_osm_end_coords_address).done(function (searchCoordinates) { 2811 if (!searchCoordinates || searchCoordinates === null) { 2812 mptbm_search_loading(parent, false); 2813 showLocationError(end_place, 'Invalid dropoff location. Please select a valid address.'); 2814 end_place.focus(); 2815 return; 2816 } 2817 2818 startCoordinates = dropdownCoords; 2819 endCoordinates = searchCoordinates; 2820 2821
2821 submitFixedZoneSearch(); 2822 }); 2823 } else { 2824 // fixed_zone_dropoff: pickup = map search, dropoff = dropdown 2825 searchInput = start_place; 2826 dropdownCoords = window.mptbm_fixed_zone_end_coords || null; 2827 2828 if (!dropdownCoords) { 2829 mptbm_search_loading(parent, false); 2830 showLocationError(end_place, 'Please select a dropoff location from the dropdown.'); 2831 parent.find("#mptbm_manual_end_place").focus(); 2832 return; 2833 } 2834 2835 let searchInputValue = getElementValue(searchInput); 2836 getCachedOrFreshCoordinates(searchInputValue, window.mptbm_fixed_zone_start_coords, window.mptbm_osm_start_coords_address).done(function (searchCoordinates) { 2837 if (!searchCoordinates || searchCoordinates === null) { 2838 mptbm_search_loading(parent, false); 2839 showLocationError(start_place, 'Invalid pickup location. Please select a valid address.'); 2840 start_place.focus(); 2841 return; 2842 } 2843 2844 startCoordinates = searchCoordinates; 2845 endCoordinates = dropdownCoords; 2846 2847 submitFixedZoneSearch(); 2848 }); 2849 } 2850 2851 function submitFixedZoneSearch() { 2852 let start_val = getElementValue(start_place); 2853 let end_val = getElementValue(end_place); 2854 if (start_val && end_val && start_date && 2855 (start_time !== undefined && start_time !== null && start_time !== '') && 2856 return_date && 2857 (return_time !== undefined && return_time !== null && return_time !== '')) { 2858 let actionValue; 2859 if (!mptbm_enable_view_search_result_page) { 2860 actionValue = "get_mptbm_map_search_result"; 2861 $.ajax({ 2862 type: "POST", 2863 url: mp_ajax_url, 2864 data: { 2865 action: actionValue, 2866 nonce: mptbm_ajax.search_nonce, 2867 mptbm_source_vehicle_id: mptbm_source_vehicle_id, 2868 start_place: start_val, 2869 start_place_coordinates: JSON.stringify(startCoordinates), 2870 end_place_coordinates: JSON.stringify(endCoordinates), 2871 end_place: end_val, 2872 start_date: start_date, 2873 start_time: start_time, 2874 price_based: price_based, 2875 two_way: two_way, 2876 waiting_time: waiting_time, 2877 fixed_time: fixed_time, 2878 return_date: return_date, 2879 return_time: return_time, 2880 mptbm_passengers: parent.find('#mptbm_passengers').val(), 2881 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 2882 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 2883 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 2884 mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent), 2885 mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent), 2886 mptbm_original_price_base: mptbm_original_price_base, 2887 mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(), 2888 mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(), 2889 }, 2890 success: function (data) { 2891 if (data.success === false) { 2892 alert(data.data.message || 'An error occurred. Please try again.'); 2893 mptbm_search_loading(parent, false); 2894 return; 2895 } 2896 target.append(data).promise().done(function () { 2897 mptbm_search_loading(parent, false); 2898 mptbm_reveal_inline_results(target); 2899 if (mptbm_is_ios()) { 2900 target[0].style.display = 'none'; 2901 void target[0].offsetHeight; 2902 target[0].style.display = ''; 2903 } 2904 }); 2905 }, 2906 error: function (response) { 2907 console.log(response); 2908 }, 2909 }); 2910 } else { 2911 actionValue = "get_mptbm_map_search_result_redirect"; 2912 $.ajax({ 2913 type: "POST", 2914 url: mp_ajax_url, 2915 data: { 2916 action: actionValue, 2917 nonce: mptbm_ajax.search_nonce, 2918 mptbm_source_vehicle_id: mptbm_source_vehicle_id, 2919 start_place: start_val, 2920 start_place_coordinates: JSON.stringify(startCoordinates), 2921 end_place_coordinates: JSON.stringify(endCoordinates), 2922 end_place: end_val, 2923 start_date: start_date, 2924 start_time: start_time, 2925 price_based: price_based, 2926 two_way: two_way, 2927 waiting_time: waiting_time, 2928 fixed_time: fixed_time, 2929 return_date: return_date, 2930 return_time: return_time, 2931 mptbm_passengers: parent.find('#mptbm_passengers').val(), 2932 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 2933 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 2934 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 2935 mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent), 2936 mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent), 2937 mptbm_original_price_base: mptbm_original_price_base, 2938 mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(), 2939 mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(), 2940 }, 2941 success: function (data) { 2942 if (data.success === false) { 2943 alert(data.data.message || 'An error occurred. Please try again.'); 2944 mptbm_search_loading(parent, false); 2945 return; 2946 } 2947 2948 var redirectUrl = mptbm_resolve_redirect_url(data); 2949 if (!redirectUrl) { 2950 mptbm_search_loading(parent, false); 2951 alert('Unable to open the search results page. Please try again.'); 2952 return; 2953 } 2954 2955 window.location.href = redirectUrl; 2956 }, 2957 error: function (response) { 2958 console.log(response); 2959 }, 2960 }); 2961 } 2962 } 2963 } 2964 2965 return; // Exit early for fixed_zone 2966 } 2967 2968 $.when( 2969 getCachedOrFreshCoordinates(start_place.value, window.mptbm_fixed_zone_start_coords, window.mptbm_osm_start_coords_address), 2970 getCachedOrFreshCoordinates(end_place.value, window.mptbm_fixed_zone_end_coords, window.mptbm_osm_end_coords_address) 2971 ).done(function (startCoordinates, endCoordinates) { 2972 // Validate that geocoding was successful 2973 if (!startCoordinates || startCoordinates === null) { 2974 mptbm_search_loading(parent, false); 2975 showLocationError(start_place, 'Invalid pickup location. Please select a valid address.'); 2976 start_place.focus(); 2977 return; 2978 } 2979 2980 if (!endCoordinates || endCoordinates === null) { 2981 mptbm_search_loading(parent, false); 2982 showLocationError(end_place, 'Invalid dropoff location. Please select a valid address.'); 2983 end_place.focus(); 2984 return; 2985 } 2986 2987 if (start_place.value && end_place.value && start_date && 2988 (start_time !== undefined && start_time !== null && start_time !== '') && 2989 return_date && 2990 (return_time !== undefined && return_time !== null && return_time !== '')) { 2991 let actionValue; 2992 if (!mptbm_enable_view_search_result_page) { 2993 actionValue = "get_mptbm_map_search_result"; 2994 $.ajax({ 2995 type: "POST", 2996 url: mp_ajax_url, 2997 data: { 2998 action: actionValue, 2999 nonce: mptbm_ajax.search_nonce, 3000 mptbm_source_vehicle_id: mptbm_source_vehicle_id, 3001 start_place: start_place.value, 3002 start_place_coordinates: startCoordinates, 3003 end_place_coordinates: endCoordinates, 3004 end_place: end_place.value, 3005 start_date: start_date, 3006 start_time: start_time, 3007 price_based: price_based, 3008 two_way: two_way, 3009 waiting_time: waiting_time, 3010 fixed_time: fixed_time, 3011 return_date: return_date, 3012 return_time: return_time, 3013 mptbm_passengers: parent.find('#mptbm_passengers').val(), 3014 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 3015 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 3016 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 3017 mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent), 3018 mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent), 3019 mptbm_original_price_base: mptbm_original_price_base, 3020 mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(), 3021 mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(), 3022 }, 3023 beforeSend: function () { 3024 //mptbm_search_loading(parent, true); 3025 }, 3026 success: function (data) { 3027 // Check if the response is an error 3028 if (data.success === false) { 3029 alert(data.data.message || 'An error occurred. Please try again.'); 3030 mptbm_search_loading(parent, false); 3031 return; 3032 } 3033 3034 target 3035 .append(data) 3036 .promise() 3037 .done(function () { 3038 mptbm_search_loading(parent, false); 3039 mptbm_reveal_inline_results(target); 3040 // iOS DOM reflow workaround 3041 if (mptbm_is_ios()) { 3042 target[0].style.display = 'none'; 3043 void target[0].offsetHeight; 3044 target[0].style.display = ''; 3045 } 3046 }); 3047 }, 3048 error: function (response) { 3049 console.log(response); 3050 }, 3051 }); 3052 } else { 3053 actionValue = "get_mptbm_map_search_result_redirect"; 3054 $.ajax({ 3055 type: "POST", 3056 url: mp_ajax_url, 3057 data: { 3058 action: actionValue, 3059 nonce: mptbm_ajax.search_nonce, 3060 mptbm_source_vehicle_id: mptbm_source_vehicle_id, 3061 start_place: start_place.value, 3062 start_place_coordinates: startCoordinates, 3063 end_place_coordinates: endCoordinates, 3064 end_place: end_place.value, 3065 start_date: start_date, 3066 start_time: start_time, 3067 price_based: price_based, 3068 two_way: two_way, 3069 waiting_time: waiting_time, 3070
3070fixed_time: fixed_time, 3071 return_date: return_date, 3072 return_time: return_time, 3073 mptbm_enable_view_search_result_page: mptbm_enable_view_search_result_page, 3074 mptbm_passengers: parent.find('#mptbm_passengers').val(), 3075 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 3076 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 3077 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 3078 mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent), 3079 mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent), 3080 mptbm_original_price_base: mptbm_original_price_base, 3081 mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(), 3082 mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(), 3083 }, 3084 beforeSend: function () { 3085 mptbm_search_loading(parent, true); 3086 }, 3087 success: function (data) { 3088 // Check if the response is an error 3089 if (data.success === false) { 3090 alert(data.data.message || 'An error occurred. Please try again.'); 3091 mptbm_search_loading(parent, false); 3092 return; 3093 } 3094 3095 var redirectUrl = mptbm_resolve_redirect_url(data); 3096 if (!redirectUrl) { 3097 mptbm_search_loading(parent, false); 3098 alert('Unable to open the search results page. Please try again.'); 3099 return; 3100 } 3101 3102 window.location.href = redirectUrl; 3103 }, 3104 error: function (response) { 3105 console.log(response); 3106 }, 3107 }); 3108 } 3109 } 3110 }); 3111 } else { 3112 3113 if (start_place.value && end_place.value && start_date && 3114 (start_time !== undefined && start_time !== null && start_time !== '') && 3115 return_date && 3116 (return_time !== undefined && return_time !== null && return_time !== '')) { 3117 3118 let actionValue; 3119 if (!mptbm_enable_view_search_result_page) { 3120 actionValue = "get_mptbm_map_search_result"; 3121 $.ajax({ 3122 type: "POST", 3123 url: mp_ajax_url, 3124 data: { 3125 action: actionValue, 3126 nonce: mptbm_ajax.search_nonce, 3127 mptbm_source_vehicle_id: mptbm_source_vehicle_id, 3128 start_place: start_place.value, 3129 start_place_coordinates: manualStartCoordinates ? JSON.stringify(manualStartCoordinates) : '', 3130 end_place: end_place.value, 3131 end_place_coordinates: manualEndCoordinates ? JSON.stringify(manualEndCoordinates) : '', 3132 start_date: start_date, 3133 start_time: start_time, 3134 price_based: price_based, 3135 two_way: two_way, 3136 waiting_time: waiting_time, 3137 fixed_time: fixed_time, 3138 return_date: return_date, 3139 return_time: return_time, 3140 mptbm_passengers: parent.find('#mptbm_passengers').val(), 3141 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 3142 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 3143 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 3144 mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent), 3145 mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent), 3146 mptbm_original_price_base: mptbm_original_price_base, 3147 mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(), 3148 mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(), 3149 mptbm_distance_text: parent.find('input[name="mptbm_hidden_distance_text"]').val(), 3150 mptbm_duration_text: parent.find('input[name="mptbm_hidden_duration_text"]').val(), 3151 }, 3152 beforeSend: function () { 3153 //mptbm_search_loading(parent, true); 3154 }, 3155 success: function (data) { 3156 // Check if the response is an error 3157 if (data.success === false) { 3158 alert(data.data.message || 'An error occurred. Please try again.'); 3159 mptbm_search_loading(parent, false); 3160 return; 3161 } 3162 3163 target 3164 .append(data) 3165 .promise() 3166 .done(function () { 3167 mptbm_search_loading(parent, false); 3168 mptbm_reveal_inline_results(target); 3169 // iOS DOM reflow workaround 3170 if (mptbm_is_ios()) { 3171 target[0].style.display = 'none'; 3172 void target[0].offsetHeight; 3173 target[0].style.display = ''; 3174 } 3175 }); 3176 }, 3177 error: function (response) { 3178 console.log(response); 3179 }, 3180 }); 3181 } else { 3182 actionValue = "get_mptbm_map_search_result_redirect"; 3183 $.ajax({ 3184 type: "POST", 3185 url: mp_ajax_url, 3186 data: { 3187 action: actionValue, 3188 nonce: mptbm_ajax.search_nonce, 3189 mptbm_source_vehicle_id: mptbm_source_vehicle_id, 3190 start_place: start_place.value, 3191 start_place_coordinates: manualStartCoordinates ? JSON.stringify(manualStartCoordinates) : '', 3192 end_place: end_place.value, 3193 end_place_coordinates: manualEndCoordinates ? JSON.stringify(manualEndCoordinates) : '', 3194 start_date: start_date, 3195 start_time: start_time, 3196 price_based: price_based, 3197 two_way: two_way, 3198 waiting_time: waiting_time, 3199 fixed_time: fixed_time, 3200 return_date: return_date, 3201 return_time: return_time, 3202 mptbm_enable_view_search_result_page: mptbm_enable_view_search_result_page, 3203 mptbm_passengers: parent.find('#mptbm_passengers').val(), 3204 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 3205 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 3206 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 3207 mptbm_extra_stop_place: mptbm_collect_extra_stop_places(parent), 3208 mptbm_extra_stop_place_coordinates: mptbm_collect_extra_stop_coordinates(parent), 3209 mptbm_original_price_base: mptbm_original_price_base, 3210 mptbm_distance: parent.find('#mptbm_calculated_distance').val() || parent.find('input[name="mptbm_hidden_distance"]').val(), 3211 mptbm_duration: parent.find('#mptbm_calculated_duration').val() || parent.find('input[name="mptbm_hidden_duration"]').val(), 3212 mptbm_distance_text: parent.find('input[name="mptbm_hidden_distance_text"]').val(), 3213 mptbm_duration_text: parent.find('input[name="mptbm_hidden_duration_text"]').val(), 3214 }, 3215 beforeSend: function () { 3216 mptbm_search_loading(parent, true); 3217 }, 3218 success: function (data) { 3219 // Check if the response is an error 3220 if (data.success === false) { 3221 alert(data.data.message || 'An error occurred. Please try again.'); 3222 mptbm_search_loading(parent, false); 3223 return; 3224 } 3225 3226 var redirectUrl = mptbm_resolve_redirect_url(data); 3227 if (!redirectUrl) { 3228 mptbm_search_loading(parent, false); 3229 alert('Unable to open the search results page. Please try again.'); 3230 return; 3231 } 3232 3233 window.location.href = redirectUrl; 3234 }, 3235 error: function (response) { 3236 console.log(response); 3237 }, 3238 }); 3239 } 3240 } 3241 } 3242 } 3243 }); 3244 $(document).on("change", "#mptbm_map_start_date", function (e, meta) { 3245 let parent = $(this).closest(".mptbm_transport_search_area"); 3246 // Clear the time slots list 3247 parent.find('#mptbm_map_start_time').siblings('.start_time_list').empty(); 3248 parent.find('.start_time_input,#mptbm_map_start_time').val(''); 3249 let mptbm_enable_return_in_different_date = $('[name="mptbm_enable_return_in_different_date"]').val(); 3250 let mptbm_buffer_end_minutes = parseInt($('[name="mptbm_buffer_end_minutes"]').val()) || 0; 3251 let mptbm_first_calendar_date = $('[name="mptbm_first_calendar_date"]').val(); 3252 3253 var selectedDate = $('#mptbm_map_start_date').val(); 3254 var formattedDate = flatpickr.parseDate(selectedDate, 'Y-m-d'); 3255 3256 // Get today's date in YYYY-MM-DD format 3257 var today = new Date(); 3258 var day = String(today.getDate()).padStart(2, '0');
3259 var month = String(today.getMonth() + 1).padStart(2, '0'); 3260 var year = today.getFullYear(); 3261 var currentDate = year + '-' + month + '-' + day; 3262 3263 if (selectedDate == currentDate) { 3264 // For today's date, apply buffer time restrictions 3265 var currentTime = new Date(); 3266 var currentHour = currentTime.getHours(); 3267 var currentMinutes = currentTime.getMinutes(); 3268 var currentTotalMinutes = (currentHour * 60) + currentMinutes; 3269 3270 $('.start_time_list-no-dsiplay li').each(function () { 3271 const timeValue = parseFloat($(this).attr('data-value')); 3272 const timeInMinutes = Math.floor(timeValue) * 60 + ((timeValue % 1) * 100); 3273 3274 // Only show times that are after the buffer period 3275 if (timeInMinutes > mptbm_buffer_end_minutes) { 3276 $('#mptbm_map_start_time').siblings('.start_time_list').append($(this).clone()); 3277 } 3278 }); 3279 } else if (selectedDate == mptbm_first_calendar_date) { 3280 3281 // For the first available date (which might be today or tomorrow depending on buffer) 3282 $('.start_time_list-no-dsiplay li').each(function () { 3283 const timeValue = parseFloat($(this).attr('data-value')); 3284 const timeInMinutes = Math.floor(timeValue) * 60 + ((timeValue % 1) * 100); 3285 3286 // If this is tomorrow and buffer extends to tomorrow, apply buffer 3287 if (mptbm_buffer_end_minutes > 1440) { 3288 const adjustedBufferMinutes = mptbm_buffer_end_minutes - 1440; 3289 if (timeInMinutes > adjustedBufferMinutes) { 3290 $('#mptbm_map_start_time') 3291 .siblings('.start_time_list') 3292 .append($(this).clone()); 3293 } 3294 3295 } else if (mptbm_buffer_end_minutes < 1440 && mptbm_buffer_end_minutes > 0) { 3296 // â If buffer does not extend to tomorrow, show time after buffer end time 3297 if (timeInMinutes >= mptbm_buffer_end_minutes) { 3298 $('#mptbm_map_start_time') 3299 .siblings('.start_time_list') 3300 .append($(this).clone()); 3301 } 3302 3303 } else { 3304 // For other dates or no buffer, show all times 3305 $('#mptbm_map_start_time') 3306 .siblings('.start_time_list') 3307 .append($(this).clone()); 3308 } 3309 }); 3310 } 3311 else { 3312 // For future dates, show all available times 3313 $('.start_time_list-no-dsiplay li').each(function () { 3314 $('#mptbm_map_start_time').siblings('.start_time_list').append($(this).clone()); 3315 }); 3316 } 3317 3318 // Update the return date picker if needed 3319 if (mptbm_enable_return_in_different_date == 'yes') { 3320 var fpReturn = $('#mptbm_return_date')[0]; 3321 if (fpReturn && fpReturn._flatpickr) { 3322 fpReturn._flatpickr.set('minDate', formattedDate); 3323 } 3324 } 3325 3326 mptbmRefreshVehicleTimeAvailability(parent); 3327 mptbm_content_refresh(parent); 3328 // Auto-opening the time dropdown is a "guide the user to the next 3329 // field" convenience for when they've just picked a date themselves - 3330 // this same "change" event also fires once on every tab load/init 3331 // (flatpickr's onReady syncing its defaultDate into this hidden field, 3332 // see MP_Global_Function::date_picker_js) with meta.initial set, which 3333 // should rebuild the time list above but not pop its dropdown open. 3334 if (!(meta && meta.initial)) { 3335 parent 3336 .find("#mptbm_map_start_time") 3337 .closest(".mp_input_select") 3338 .find("input.formControl") 3339 .trigger("click"); 3340 } 3341 }); 3342 3343 3344 $(document).on("change", "#mptbm_map_return_date", function () { 3345 let mptbm_enable_return_in_different_date = $('[name="mptbm_enable_return_in_different_date"]').val(); 3346 3347 if (mptbm_enable_return_in_different_date == 'yes') { 3348 var selectedTime = parseFloat($('#mptbm_map_start_time').val()); 3349 var selectedDate = $('#mptbm_map_start_date').val(); 3350 var dateValue = $('#mptbm_map_return_date').val(); 3351 3352 // Check if the return date is the same as the pickup date 3353 if (selectedDate == dateValue) { 3354 $('#return_time_list').show(); 3355 // Clear existing options 3356 $('#mptbm_map_return_time').siblings('.mp_input_select_list').empty(); 3357 $('.mptbm_map_return_time_input').val(''); 3358 // If return date is the same as the pickup date, show only times after pickup time 3359 $('.mp_input_select_list li').each(function () { 3360 var timeValue = parseFloat($(this).attr('data-value')); 3361 if (timeValue > selectedTime) { 3362 $('#mptbm_map_return_time').siblings('.mp_input_select_list').append($(this).clone()); 3363 } 3364 }); 3365 } else { 3366 // Clear existing options 3367 $('#mptbm_map_return_time').siblings('.mp_input_select_list').empty(); 3368 $('.mptbm_map_return_time_input').val(''); 3369 $('.return_time_list-no-dsiplay li').each(function () { 3370 var timeValue = parseFloat($(this).attr('data-value')); 3371 $('#mptbm_map_return_time').siblings('.mp_input_select_list').append($(this).clone()); 3372 }); 3373 } 3374 } 3375 3376 // Trigger refresh and display logic 3377 let parent = $(this).closest(".mptbm_transport_search_area"); 3378 mptbm_content_refresh(parent); 3379 parent.find("#mptbm_map_return_time").closest(".mp_input_select").find("input.formControl").trigger("click"); 3380 }); 3381 3382 3383 $(document).on("click", ".start_time_list li", function () { 3384 if ($(this).attr('aria-disabled') === 'true') { 3385 return false;
3386 } 3387 let selectedValue = $(this).attr('data-value'); 3388 $(this).closest('.mptbm_transport_search_area').find('#mptbm_map_start_time').val(selectedValue).trigger('change'); 3389 }); 3390 $(document).on("click", ".return_time_list li", function () { 3391 let selectedValue = $(this).attr('data-value'); 3392 $('#mptbm_map_return_time').val(selectedValue).trigger('change'); 3393 }); 3394 $(document).on("change", "#mptbm_map_start_time", function () { 3395 let parent = $(this).closest(".mptbm_transport_search_area"); 3396 mptbm_content_refresh(parent); 3397 parent.find("#mptbm_map_start_place").focus(); 3398 }); 3399 $(document).on("change", "#mptbm_manual_start_place", function () { 3400 let parent = $(this).closest(".mptbm_transport_search_area"); 3401 parent.removeData('mptbm-manual-start-coordinates mptbm-manual-end-coordinates'); 3402 mptbm_content_refresh(parent); 3403 let start_place = $(this).val(); 3404 let target = parent.find(".mptbm_manual_end_place"); 3405 let price_based = parent.find('[name="mptbm_price_based"]').val(); 3406 3407 // For fixed_zone (pickup), place marker on map using geo coordinates 3408 if (price_based === "fixed_zone" && start_place) { 3409 let selectedOption = $(this).find('option:selected'); 3410 let geoCoords = selectedOption.data('geo'); 3411 let locationLabel = selectedOption.data('label') || selectedOption.text(); 3412 3413 3414 3415 if (geoCoords) { 3416 let coords = geoCoords.split(','); 3417 let lat = parseFloat(coords[0]); 3418 let lng = parseFloat(coords[1]); 3419 3420 if (!isNaN(lat) && !isNaN(lng)) { 3421 // Check if using OpenStreetMap or Google Maps 3422 var mapType = document.getElementById('mptbm_map_type'); 3423 3424 if (mapType && mapType.value === 'openstreetmap') { 3425 // OpenStreetMap (Leaflet) 3426 if (typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map) { 3427 // Clear existing markers 3428 if (typeof mptbm_osm_start_marker !== 'undefined' && mptbm_osm_start_marker) { 3429 mptbm_osm_map.removeLayer(mptbm_osm_start_marker); 3430 } 3431 // Add new marker 3432 mptbm_osm_start_marker = L.marker([lat, lng]).addTo(mptbm_osm_map); 3433 mptbm_osm_map.setView([lat, lng], 14); 3434 3435 // Store coordinates for later use 3436 window.mptbm_fixed_zone_start_coords = { latitude: lat, longitude: lng }; 3437 3438 3439 // Calculate route if end marker exists 3440 if (typeof mptbm_osm_end_marker !== 'undefined' && mptbm_osm_end_marker) { 3441 mptbm_calculate_osm_distance(); 3442 } 3443 } else { 3444 3445 } 3446 } else if (typeof google !== 'undefined' && typeof google.maps !== 'undefined') { 3447 // Google Maps 3448 if (typeof mptbm_map !== 'undefined' && mptbm_map) { 3449 var latLng = new google.maps.LatLng(lat, lng); 3450 3451 // Clear existing start marker 3452 if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) { 3453 mptbm_start_marker.setMap(null); 3454 } 3455 3456 // Add new marker 3457 mptbm_start_marker = new google.maps.Marker({ 3458 position: latLng, 3459 map: mptbm_map, 3460 title: locationLabel 3461 }); 3462 3463 mptbm_map.setCenter(latLng); 3464 mptbm_map.setZoom(14); 3465 3466 // Store coordinates for later use 3467 window.mptbm_fixed_zone_start_coords = { latitude: lat, longitude: lng }; 3468 3469 3470 // Update distance cookie for Google Maps rout
3470ing 3471 var latLngStr = lat + "," + lng; 3472 var endPlace = document.getElementById('mptbm_map_end_place'); 3473 mptbm_set_cookie_distance_duration(latLngStr, endPlace ? endPlace.value : ''); 3474 3475 // Calculate route if end marker exists 3476 if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) { 3477 mptbm_calculate_google_route_from_markers(); 3478 } 3479 } 3480 } 3481 } 3482 } 3483 3484 3485 } else { 3486 // Reset start coordinates if no place selected 3487 window.mptbm_fixed_zone_start_coords = null; 3488 } 3489 3490 if (start_place) { 3491 let end_place = ""; 3492 if (price_based === "manual") { 3493 let post_id = parent.find('[name="mptbm_post_id"]').val() || parent.find('[name="mptbm_source_vehicle_id"]').val(); 3494 $.ajax({ 3495 type: "POST", 3496 url: mp_ajax_url, 3497 data: { 3498 action: "get_mptbm_end_place", 3499 nonce: mptbm_ajax.search_nonce, 3500 start_place: start_place, 3501 price_based: price_based, 3502 post_id: post_id, 3503 }, 3504 beforeSend: function () { 3505 // Remove any existing custom dropdown before AJAX call 3506 $('.mptbm-custom-select-wrapper').remove(); 3507 dLoader(target.closest(".mptbm_search_area")); 3508 }, 3509 success: function (data) { 3510 target 3511 .html(data) 3512 .promise() 3513 .done(function () { 3514 dLoaderRemove(target.closest(".mptbm_search_area")); 3515 // iOS DOM reflow workaround 3516 if (mptbm_is_ios()) { 3517 target[0].style.display = 'none'; 3518 void target[0].offsetHeight; 3519 target[0].style.display = ''; 3520 } 3521 3522 // Add a small delay to ensure the select is properly updated 3523 setTimeout(function () { 3524 //console.log('Select updated, options count:', target.find('option:not([disabled])').length); 3525 }, 100); 3526 }); 3527 }, 3528 error: function (response) { 3529 console.log('AJAX error for end locations:', response); 3530 }, 3531 }); 3532 } 3533 } 3534 }); 3535 $(document).on("change", "#mptbm_manual_end_place", function () { 3536 let parent = $(this).closest(".mptbm_transport_search_area"); 3537 parent.removeData('mptbm-manual-end-coordinates'); 3538 mptbm_content_refresh(parent); 3539 let end_place = $(this).val(); 3540 let price_based = parent.find('[name="mptbm_price_based"]').val(); 3541 3542 // For fixed_zone_dropoff, place marker on map using geo coordinates 3543 if (price_based === "fixed_zone_dropoff" && end_place) { 3544 let selectedOption = $(this).find('option:selected'); 3545 let geoCoords = selectedOption.data('geo'); 3546 let locationLabel = selectedOption.data('label') || selectedOption.text(); 3547 3548 3549 3550 if (geoCoords) { 3551 let coords = geoCoords.split(','); 3552 let lat = parseFloat(coords[0]); 3553 let lng = parseFloat(coords[1]); 3554 3555 if (!isNaN(lat) && !isNaN(lng)) { 3556 var mapType = document.getElementById('mptbm_map_type'); 3557 3558 if (mapType && mapType.value === 'openstreetmap') { 3559 if (typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map) { 3560 if (typeof mptbm_osm_end_marker !== 'undefined' && mptbm_osm_end_marker) { 3561 mptbm_osm_map.removeLayer(mptbm_osm_end_marker); 3562 } 3563 mptbm_osm_end_marker = L.marker([lat, lng]).addTo(mptbm_osm_map);
3564 mptbm_osm_map.setView([lat, lng], 14); 3565 window.mptbm_fixed_zone_end_coords = { latitude: lat, longitude: lng }; 3566 3567 // Calculate route if start marker exists 3568 if (typeof mptbm_osm_start_marker !== 'undefined' && mptbm_osm_start_marker) { 3569 mptbm_calculate_osm_distance(); 3570 } 3571 } 3572 } else if (typeof google !== 'undefined' && typeof google.maps !== 'undefined') { 3573 if (typeof mptbm_map !== 'undefined' && mptbm_map) { 3574 var latLng = new google.maps.LatLng(lat, lng); 3575 if (typeof mptbm_end_marker !== 'undefined' && mptbm_end_marker) { 3576 mptbm_end_marker.setMap(null); 3577 } 3578 mptbm_end_marker = new google.maps.Marker({ 3579 position: latLng, 3580 map: mptbm_map, 3581 title: locationLabel 3582 }); 3583 mptbm_map.setCenter(latLng); 3584 mptbm_map.setZoom(14); 3585 window.mptbm_fixed_zone_end_coords = { latitude: lat, longitude: lng }; 3586 3587 3588 // Update distance cookie for Google Maps 3589 var latLngStr = lat + "," + lng; 3590 var startPlace = document.getElementById('mptbm_map_start_place'); 3591 mptbm_set_cookie_distance_duration(startPlace ? startPlace.value : '', latLngStr); 3592 3593 // Calculate route if start marker exists 3594 if (typeof mptbm_start_marker !== 'undefined' && mptbm_start_marker) { 3595 mptbm_calculate_google_route_from_markers(); 3596 } 3597 } 3598 } 3599 } 3600 } 3601 } else { 3602 // Reset end coordinates if no place selected 3603 window.mptbm_fixed_zone_end_coords = null; 3604 } 3605 3606 if (price_based === 'manual' && end_place) { 3607 mptbm_update_manual_route_map(parent); 3608 } 3609 }); 3610 $(document).on("change", "#mptbm_map_start_place,#mptbm_map_end_place", function () { 3611 let parent = $(this).closest(".mptbm_transport_search_area"); 3612 mptbm_content_refresh(parent); 3613 let start_place = parent.find("#mptbm_map_start_place").val(); 3614 let end_place = parent.find("#mptbm_map_end_place").val(); 3615 if (start_place || end_place) { 3616 if (start_place) { 3617 mptbm_set_cookie_distance_duration(start_place); 3618 parent.find("#mptbm_map_end_place").focus(); 3619 } else { 3620 mptbm_set_cookie_distance_duration(end_place); 3621 parent.find("#mptbm_map_start_place").focus(); 3622 } 3623 } else { 3624 parent.find("#mptbm_map_start_place").focus(); 3625 } 3626 } 3627 ); 3628 $(document).on("change", ".mptbm_transport_search_area [name='mptbm_taxi_return']", function () { 3629 let parent = $(this).closest(".mptbm_transport_search_area"); 3630 mptbm_content_refresh(parent); 3631 } 3632 ); 3633 $(document).on( 3634 "change", 3635 ".mptbm_transport_search_area [name='mptbm_waiting_time']", 3636 function () { 3637 let parent = $(this).closest(".mptbm_transport_search_area"); 3638 mptbm_content_refresh(parent); 3639 } 3640 ); 3641})(jQuery); 3642 3643function mptbm_content_refresh(parent) { 3644 parent.find('[name="mptbm_post_id"]').val(""); 3645 parent.find(".mptbm_map_search_result").remove(); 3646 parent.find(".mptbm_order_summary").remove(); 3647} 3648// Route planning + map now stay visible while results load inline below the 3649// map (instead of switching to a separate step/tab under a full dark 3650// overlay) - show a lightweight in-button spinner instead of blurring the 3651// whole panel. 3652// The AJAX response's outer wrapper carries data-tabs-next="#mptbm_search_result", 3653// which the shared tab-pane CSS (.tabsContentNext [data-tabs-next]) hides by 3654// default unless it has .active - normally added by switching to that tab.
3655// We're no longer switching tabs (results render inline below the map 3656// instead), so mark it active and drop the attribute directly. 3657// The OSM map keeps whatever zoom/center it had before its container was 3658// resized - Leaflet has no way to know the container changed size on its 3659// own, so a collapse/expand leaves it showing a cropped fragment of the 3660// route rather than re-fitting to it. invalidateSize() tells Leaflet to 3661// re-measure its container, then re-fitting to the route's own bounds (or 3662// the marker group, before a route line exists) zooms so the whole trip is 3663// visible at whatever height the map currently has. 3664function mptbm_refit_osm_map() { 3665 if (typeof mptbm_osm_map === 'undefined' || !mptbm_osm_map) { 3666 return; 3667 } 3668 setTimeout(function () { 3669 mptbm_osm_map.invalidateSize(); 3670 if (typeof mptbm_osm_route !== 'undefined' && mptbm_osm_route) { 3671 mptbm_osm_map.fitBounds(mptbm_osm_route.getBounds().pad(0.1)); 3672 return; 3673 } 3674 var markers = [ 3675 typeof mptbm_osm_start_marker !== 'undefined' ? mptbm_osm_start_marker : null, 3676 typeof mptbm_osm_end_marker !== 'undefined' ? mptbm_osm_end_marker : null, 3677 typeof mptbm_osm_extra_marker !== 'undefined' ? mptbm_osm_extra_marker : null, 3678 ].filter(Boolean); 3679 if (markers.length > 0) { 3680 var group = new L.featureGroup(markers); 3681 mptbm_osm_map.fitBounds(group.getBounds().pad(0.1)); 3682 } 3683 }, 320); 3684} 3685// Sets the "1 Enter Ride Details / 2 Choose a vehicle / 3 Place Order" step 3686// indicator (.tabListsNext, top of the whole booking flow) to reflect 3687// targetSelector as the current step - mirrors what mp_script.js's 3688// active_next_tab() does to these same classes/icons/text when its 3689// nextTab_next/nextTab_prev buttons are clicked, but written as an explicit, 3690// idempotent set instead of reusing that function directly: that one also
3691// slides .tabsContentNext panels open/closed, adjusts page scroll, and (via 3692// mp_all_content_change) unconditionally *toggles* each step's checkmark/ 3693// class/text - fine for a single click, but calling it every time results 3694// load (including repeat searches) would flip the checkmark back and forth 3695// instead of staying put once a step is done. 3696function mptbm_set_step_active(parent, targetSelector) { 3697 var $stepList = parent.find('.tabListsNext').first(); 3698 if (!$stepList.length) { 3699 return; 3700 } 3701 var $steps = $stepList.children('[data-tabs-target-next]'); 3702 var targetIndex = $steps.filter('[data-tabs-target-next="' + targetSelector + '"]').index() + 1; 3703 if (targetIndex < 1) { 3704 return; 3705 } 3706 $steps.each(function (i) { 3707 var stepNum = i + 1; 3708 var $step = jQuery(this); 3709 $step.toggleClass('active', stepNum <= targetIndex); 3710 // Steps strictly before the current one are "done" - swap their 3711 // number for a checkmark (data-open-icon/-text vs data-close-icon/ 3712 // -text, same attributes mp_script.js's content_*_change() read). 3713 var isDone = stepNum < targetIndex; 3714 var addClass = $step.data('add-class'); 3715 if (addClass) { 3716 var $classTarget = $step.find('[data-class]').length ? $step.find('[data-class]') : $step; 3717 $classTarget.toggleClass(addClass, isDone); 3718 } 3719 var openIcon = $step.data('open-icon'); 3720 var closeIcon = $step.data('close-icon'); 3721 if (openIcon || closeIcon) { 3722 var $iconTarget = $step.find('[data-icon]'); 3723 if (isDone) { 3724 $iconTarget.removeClass(openIcon).addClass(closeIcon); 3725 } else { 3726 $iconTarget.removeClass(closeIcon).addClass(openIcon); 3727 } 3728 } 3729 var openText = $step.data('open-text') != null ? $step.data('open-text').toString() : ''; 3730 var closeText = $step.data('close-text') != null ? $step.data('close-text').toString() : ''; 3731 if (openText || closeText) { 3732 $step.find('[data-text]').html(isDone ? closeText : openText); 3733 } 3734 }); 3735} 3736function mptbm_reveal_inline_results(target) { 3737 target.find('[data-tabs-next]').addClass('active').removeAttr('data-tabs-next'); 3738 target.addClass('mptbm_inline_results_active'); 3739 // Results live inside .mptbm_map_area (a sibling of the collapsible map 3740 // body), taking over that same column. For "manual" pricing the whole 3741 // column starts display:none (no map at all) - force it visible now 3742 // that it has results to show. Then auto-collapse just the map body 3743 // (still toggleable back open) since results are the priority now. 3744 var $mapArea = target.closest('.mptbm_map_area'); 3745 // Flat-rate/"manual" pricing has no real map at all - no geocoding, just 3746 // named location dropdowns. .mptbm_inline_search_results still lives 3747 // nested inside .mptbm_map_area though (see below), so that column still 3748 // needs to be revealed to show these results - just without any of the 3749 // map-specific chrome/collapse/refit logic, which would otherwise force 3750 // open a blank gray box sized for a map that was never there. 3751 // 3752 // Can't tell "no map" apart from "map not initialized yet" just by 3753 // checking mptbm_osm_map's truthiness - on the tabs page it's one shared 3754 // global var, so a map already initialized for a different tab (e.g. the 3755 // default "Hourly" tab on page load) stays truthy even after switching to 3756 // Flat Rate. Read this specific tab/form's own price_based value instead 3757 // - the same field the PHP template itself keys the map's display on. 3758 var $searchAreaRoot = $mapArea.closest('.mptbm_transport_search_area'); 3759 var priceBased = $searchAreaRoot.find('input[name="mptbm_price_based"]').val(); 3760 // Deliberately NOT reading data-map here - that's the shortcode/block's 3761 // own "map" option and only governs the pre-search form state. Once 3762 // results are revealed, visibility is instead driven by the separate 3763 // global admin switch (Map API Settings > Show Map on Search Result 3764 // Page), rendered into data-show-map-result in get_details.php. Without 3765 // this check, that setting was being silently ignored here and the map 3766 // always forced back to visible once results were shown. 3767 var showMapResult = ($mapArea.attr('data-show-map-result') || 'yes').toLowerCase(); 3768 var manualMapEnabled = ($mapArea.attr('data-manual-map') || 'no').toLowerCase() === 'yes'; 3769 var hasMap = (priceBased !== 'manual' || manualMapEnabled) && showMapResult !== 'no'; 3770 // Results now show inline on step 1's own panel instead of switching to a 3771 // separate step-2 panel, but the step indicator above it should still 3772 // read as "Choose a vehicle" being current now that there's something to 3773 // choose from. 3774 mptbm_set_step_active($searchAreaRoot, '#mptbm_search_result'); 3775 if ($mapArea.length) { 3776 if (hasMap) { 3777 // mptbm_map_collapsed toggles on/off as the user opens/closes the 3778 // map peek - mptbm_results_shown never comes back off, marking 3779 // "results are now sharing this column with the map" for CSS 3780 // that needs to tell that apart from the pre-search state (see 3781 // the .fullHeight height override below). 3782 $mapArea.css('display', 'flex').addClass('mptbm_map_collapsed mptbm_results_shown'); 3783 var $toggle = $mapArea.find('.mptbm_map_collapse_toggle'); 3784 $toggle.attr('aria-expanded', 'false'); 3785 $toggle.find('[data-label]').text($toggle.data('expand-text')); 3786 mptbm_refit_osm_map(); 3787 } else { 3788 $mapArea.css('display', 'flex').addClass('mptbm_map_area_no_map'); 3789 } 3790 // The Total Distance/Total Time bar above the map is written from the 3791 // browser's own Directions result. The prices in these results were 3792 // calculated from a separate server-side lookup, which is allowed to 3793 // disagree with it - different provider (Google here, OSRM whenever the 3794 // server-side Google request is refused), so a different road and a 3795 // different number. Leaving the browser's figure on screen next to a 3796 // fare derived from the other one is how a trip ends up displaying 3797 // "11.9 km" while being quoted for 22.5 km. Once results exist, the 3798 // priced numbers win. Only overwritten when the server actually 3799 // verified a distance (choose_vehicles.php leaves these empty 3800 // otherwise), so the client-side estimate still stands on its own when 3801 // there was no server-side lookup to trust. 3802 var pricedDistanceText = target.find('.mptbm_priced_distance_text').first().val(); 3803 var pricedDurationText = target.find('.mptbm_priced_duration_text').first().val(); 3804 if (pricedDistanceText) { 3805 $mapArea.find('.mptbm_distance_time .mptbm_total_distance').text(pricedDistanceText); 3806 } 3807 if (pricedDurationText) { 3808 $mapArea.find('.mptbm_distance_time .mptbm_total_time').text(pricedDurationText); 3809 } 3810 // Fold Total Distance/Total Time into the trip-summary card's own 3811 // meta row too, alongside Duration/Pickup Date/Pickup Time - reading 3812 // the current text rather than moving the live .mptbm_total_distance/ 3813 // .mptbm_total_time nodes themselves, since those keep getting 3814 // updated in place as the user adjusts locations for their *next* 3815 // search and need to still exist for that. Hiding (not removing) 3816 // the original bar keeps that live-update wiring intact underneath. 3817 // There can be several .mptbm_summary_top_row elements at once - one 3818 // per hidden .leftSidebar (summary.php renders one per pricing tab) 3819 // plus the one actually inside the visible .mptbm_results_toolbar - 3820 // .first() alone would silently grab a hidden one, so scope to the 3821 // toolbar's own row specifically. 3822 var $summaryRow = target.find('.mptbm_results_toolbar .mptbm_summary_top_row').first(); 3823 if ($summaryRow.length) { 3824 // Transfer Type and Extra Waiting Hours are their own custom 3825 // dropdown widgets (a readonly text input showing the chosen 3826 // option's label, proxying a real <select>) living in Route 3827 // Planning itself (a sibling of .mptbm_map_area, both under the 3828 // same .mptbm_transport_search_area root) - only rendered at all 3829 // when their respective settings are enabled, so each is read 3830 // only if actually present. 3831 var $transferType = $searchAreaRoot.find('[data-proxy-for="mptbm_taxi_return"]'); 3832 var $waitingHours = $searchAreaRoot.find('[data-proxy-for="mptbm_waiting_time"]'); 3833 if ($transferType.length) { 3834 var transferLabel = $transferType.find('span').first().text().trim(); 3835 var transferValue = $transferType.find('input.formControl').first().val(); 3836 if (transferValue) { 3837 $summaryRow.append( 3838 jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_transfer_type"></div>') 3839 .append(jQuery('<h6/>').text(transferLabel)) 3840 .append(jQuery('<p class="_textLight_1"/>').text(transferValue)) 3841 ); 3842 } 3843 } 3844 if ($waitingHours.length) { 3845 var waitingLabel = $waitingHours.find('span').first().text().trim(); 3846 var waitingValue = $waitingHours.find('input.formControl').first().val(); 3847 if (waitingValue) { 3848 $summaryRow.append( 3849 jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_waiting_hours"></div>') 3850 .append(jQuery('<h6/>').text(waitingLabel)) 3851 .append(jQuery('<p class="_textLight_1"/>').text(waitingValue)) 3852 ); 3853 } 3854 } 3855 } 3856 // A manual route has distance/time only when its optional map resolved 3857 // both configured locations. In that case the same verified values are 3858 // folded into this summary just like every other mapped pricing mode. 3859 var $distanceTime = hasMap ? $mapArea.find('.mptbm_distance_time') : jQuery(); 3860 if ($summaryRow.length && $distanceTime.length) { 3861 var $distanceVal = $distanceTime.find('.mptbm_total_distance').first(); 3862 var $timeVal = $distanceTime.find('.mptbm_total_time').first(); 3863 var distanceText = $distanceVal.text().trim(); 3864 var timeText = $timeVal.text().trim(); 3865 // The original bar's labels are written in ALL CAPS in the source 3866 // string itself (translated, so not just a CSS transform) - title- 3867 // casing here (rather than a CSS transform, which can't lowercase 3868 // characters that are already uppercase) matches the normal-case 3869 // style of Duration/Pickup Date/Pickup Time next to them. 3870 function mptbm_title_case(str) { 3871 return str.toLowerCase().replace(/\b\w/g, function (c) { return c.toUpperCase(); }); 3872 } 3873 var distanceLabel = mptbm_title_case($distanceVal.closest('.fdColumn').find('h6').first().text().trim()); 3874 var timeLabel = mptbm_title_case($timeVal.closest('.fdColumn').find('h6').first().text().trim()); 3875 if (distanceText) { 3876 $summaryRow.append( 3877 jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_distance"></div>') 3878 .append(jQuery('<h6/>').text(distanceLabel)) 3879 .append(jQuery('<p class="_textLight_1"/>').text(distanceText)) 3880 ); 3881 } 3882 if (timeText) { 3883 $summaryRow.append( 3884 jQuery('<div class="mptbm_summary_top_col mptbm_summary_col_time_total"></div>') 3885 .append(jQuery('<h6/>').text(timeLabel)) 3886 .append(jQuery('<p class="_textLight_1"/>').text(timeText)) 3887 ); 3888 } 3889 $distanceTime.addClass('mptbm_distance_time_merged'); 3890 } 3891 } 3892 // mp_script.js's lazy-load only scans [data-bg-image] on initial page
3893 // load/scroll (loadBgImage(), guarded so it only ever runs once) - vehicle 3894 // photos appended here afterwards never get picked up otherwise, showing 3895 // as the empty gray placeholder. loadBgImage() itself has no such guard, 3896 // so calling it directly force-scans the newly appended cards' images. 3897 if (typeof loadBgImage === 'function') { 3898 loadBgImage(); 3899 } 3900} 3901function mptbm_search_loading(parent, isLoading) { 3902 var btn = parent.find('#mptbm_get_vehicle'); 3903 var icon = btn.find('.fa-search-location, .fa-spinner').first(); 3904 if (isLoading) { 3905 btn.prop('disabled', true).addClass('mptbm-searching'); 3906 icon.removeClass('fa-search-location').addClass('fa-spinner fa-spin'); 3907 } else { 3908 btn.prop('disabled', false).removeClass('mptbm-searching'); 3909 icon.removeClass('fa-spinner fa-spin').addClass('fa-search-location'); 3910 } 3911} 3912//=======================// 3913function mptbm_price_calculation(parent) { 3914 let target_summary = parent.find(".mptbm_transport_summary"); 3915 let total = 0; 3916 let post_id = parseInt(parent.find('[name="mptbm_post_id"]').val()); 3917 if (post_id > 0) { 3918 let quantityInput = parent.find(`.mp_quantity_input[data-post-id="${post_id}"]`); 3919 let quantityVal = quantityInput.length ? parseInt(quantityInput.val()) || 1 : 1; 3920 3921 // Use the unit price of transport 3922 let unit_transport_price = parseFloat(parent.find('[name="mptbm_post_id"]').data("unit-transport-price") || parent.find('[name="mptbm_post_id"]').attr("data-price") / quantityVal || 0); 3923 let base_transport_price = unit_transport_price * quantityVal; 3924 3925 let unit_base_price_extra = parseFloat(parent.find('[name="mptbm_post_id"]').attr("data-unit-base-price") || 0); 3926 let tax_multiplier_val = parseFloat(parent.find('[name="mptbm_post_id"]').attr("data-tax-multiplier") || 1); 3927 3928 3929 let base_price_extra = unit_base_price_extra * quantityVal * tax_multiplier_val; 3930 3931 total = total + base_transport_price + base_price_extra; 3932 3933 3934 parent.find(".mptbm_extra_service_item").each(function () { 3935 let service_name = jQuery(this) 3936 .find('[name="mptbm_extra_service[]"]') 3937 .val(); 3938 if (service_name) { 3939 let ex_target = jQuery(this).find('[name="mptbm_extra_service_qty[]"]'); // Added missing ] also 3940 let ex_qty = parseInt(ex_target.val()); 3941 let ex_price = ex_target.data("price"); 3942 ex_price = ex_price && ex_price > 0 ? ex_price : 0; 3943 total = total + parseFloat(ex_price) * ex_qty; 3944 } 3945 }); 3946 } 3947 var el = target_summary.find(".mptbm_product_total_price"); 3948 el.html(mp_price_format(total)); 3949 // iOS DOM reflow workaround 3950 if (mptbm_is_ios()) { 3951 el.hide().show(0); 3952 } 3953} 3954 3955/** 3956 * Calculates distance from Base Location to Pickup and Dropoff to Base Location 3957 */ 3958function mptbm_calculate_base_distances(settings, pickup, dropoff, callback) { 3959 if (!settings || !settings.coords || !pickup || !dropoff) { 3960 callback({ distance: 0, duration: 0 }); 3961 return; 3962 } 3963 3964 // Check if we should use OSM (if Google is not defined or explicitly using OSM) 3965 // We check for mptbm_osm_map to see if OSM is the active map provider 3966 if (typeof google === 'undefined' || typeof mptbm_osm_map !== 'undefined') { 3967 mptbm_calculate_base_distances_osm(settings, pickup, dropoff, callback); 3968 return; 3969 } 3970 3971 let base_coords = settings.coords; 3972 let total_distance = 0; 3973 let total_duration = 0; 3974 let pending_calls = 0; 3975 3976 let check_complete = function () { 3977 if (pending_calls === 0) { 3978 callback({ distance: total_distance, duration: total_duration }); 3979 } 3980 }; 3981 3982 let calculate = function (origin, destination) { 3983 pending_calls++; 3984 let service = new google.maps.DistanceMatrixService(); 3985 service.getDistanceMatrix({ 3986 origins: [origin], 3987 destinations: [destination], 3988 travelMode: 'DRIVING', 3989 }, function (response, status) { 3990 if (status === 'OK' && response.rows[0].elements[0].status === 'OK') { 3991 total_distance += response.rows[0].elements[0].distance.value; 3992 total_duration += response.rows[0].elements[0].duration.value; 3993 } 3994 pending_calls--; 3995 check_complete(); 3996 }); 3997 }; 3998 3999 if (settings.charge_pickup === 'yes') { 4000 calculate(base_coords, pickup); 4001 } 4002 if (settings.charge_dropoff === 'yes') { 4003 calculate(dropoff, base_coords); 4004 } 4005 4006 if (pending_calls === 0) { 4007 callback({ distance: 0, duration: 0 }); 4008 } 4009} 4010(function ($) { 4011 4012 $(document).on('click', '.mp_quantity_minus, .mp_quantity_plus', function () { 4013 var postId = $(this).data('post-id'); 4014 var $input = $(`.mp_quantity_input[data-post-id="${postId}"]`); 4015 var currentVal = parseInt($input.val()); 4016 var maxVal = parseInt($input.attr('max')); 4017 var minVal = parseInt($input.attr('min')); 4018 4019 if ($(this).hasClass('mp_quantity_minus')) { 4020 if (currentVal > minVal) { 4021 $input.val(currentVal - 1); 4022 } 4023 } else { 4024 if (currentVal < maxVal) { 4025 $input.val(currentVal + 1); 4026 } 4027 } 4028 4029 var updatedVal = parseInt($input.val()); 4030 var $parent = $(this).closest('.mptbm_booking_item'); 4031 var $searchArea = $parent.closest('.mptbm_transport_search_area'); 4032 var transportPrice = parseFloat($(`.mptbm_transport_select[data-post-id="${postId}"]`).attr('data-transport-price')); 4033 var $summary = $searchArea.find('.mptbm_transport_summary'); 4034 4035 // Check if there's a custom message 4036 let customMessage = $parent.find('.mptbm-custom-price-message').html(); 4037 if (customMessage) { 4038 // If there's a custom message, show it with quantity 4039 $summary.find('.mptbm_product_price').html( 4040 'x' + updatedVal + ' <span style="color:#000;">| </span>' + customMessage 4041 ); 4042 } else { 4043 // If no custom message, show price as before 4044 $summary.find('.mptbm_product_price').html( 4045 'x' + updatedVal + ' <span style="color:#000;">| </span>' + mp_price_format(transportPrice * updatedVal) 4046 ); 4047 } 4048 4049 // ð§ Update the data-price dynamically if needed 4050 $searchArea.find('[name="mptbm_post_id"]').attr('data-price', transportPrice * updatedVal); 4051 4052 // â Now update the total 4053 mptbm_price_calculation($searchArea); 4054 }); 4055 $(document).on('click', '.mptbm_transport_search_area .mptbm_transport_select', function () {
4056 let $this = $(this); 4057 let postId = $this.data('post-id'); 4058 let parent = $this.closest('.mptbm_transport_search_area'); 4059 4060 let target_summary = parent.find('.mptbm_transport_summary'); 4061 let target_extra_service = parent.find('.mptbm_extra_service'); 4062 let target_extra_service_summary = parent.find('.mptbm_extra_service_summary'); 4063 let all_quantity_selectors = parent.find('.mptbm_quantity_selector'); 4064 let target_quantity_selector = parent.find('.mptbm_quantity_selector_' + postId); 4065 4066 if (target_quantity_selector.length && target_quantity_selector.hasClass('mptbm_booking_item_hidden')) { 4067 all_quantity_selectors.addClass('mptbm_booking_item_hidden'); 4068 target_quantity_selector.removeClass('mptbm_booking_item_hidden'); 4069 } else { 4070 all_quantity_selectors.addClass('mptbm_booking_item_hidden'); 4071 } 4072 4073 target_summary.slideDown(400); 4074 target_extra_service.slideDown(400).html(''); 4075 target_extra_service_summary.slideDown(400).html(''); 4076 parent.find('[name="mptbm_post_id"]').val(''); 4077 parent.find('.mptbm_checkout_area').html(''); 4078 4079 if ($this.hasClass('active_select')) { 4080 $this.removeClass('active_select'); 4081 mp_all_content_change($this); 4082 target_summary.slideUp(400); 4083 // No vehicle selected anymore -- step 3 is no longer a preview. 4084 // Matched by data-tabs-target-next rather than .step-place-order: 4085 // that class only exists in transport_result.php's copy of this 4086 // stepper, not the one registration_layout.php renders. 4087 parent.find('[data-tabs-target-next="#mptbm_order_summary"]').removeClass('active'); 4088 } else { 4089 parent.find('.mptbm_transport_select.active_select').each(function () { 4090 $(this).removeClass('active_select'); 4091 mp_all_content_change($(this)); 4092 }).promise().done(function () { 4093 let transport_name = $this.attr('data-transport-name'); 4094 let transport_price = parseFloat($this.attr('data-transport-price')); 4095 let post_id = $this.attr('data-post-id'); 4096 4097 let quantityInput = parent.find(`.mp_quantity_input[data-post-id="${post_id}"]`); 4098 let quantityVal = quantityInput.length ? parseInt(quantityInput.val()) || 1 : 1; 4099 4100 target_summary.find('.mptbm_product_name').html(transport_name); 4101 4102 let customMessage = $this.closest('.mptbm_booking_item').find('.mptbm-custom-price-message').html(); 4103 if (customMessage) { 4104 target_summary.find('.mptbm_product_price').html( 4105 'x' + quantityVal + ' <span style="color:#000;">| </span> ' + customMessage 4106 ); 4107 } else { 4108 target_summary.find('.mptbm_product_price').html( 4109 'x' + quantityVal + ' <span style="color:#000;">| </span> ' + mp_price_format(transport_price * quantityVal) 4110 ); 4111 } 4112 4113 $this.addClass('active_select'); 4114 $('.mptbm_booking_item').removeClass('selected'); 4115 $this.closest('.mptbm_booking_item').addClass('selected'); 4116 4117 // Preview step 3 ("Place Order") as active as soon as a vehicle is 4118 // picked, ahead of the checkout content actually loading. 4119 parent.find('[data-tabs-target-next="#mptbm_order_summary"]').addClass('active'); 4120 4121 mp_all_content_change($this); 4122 4123 parent.find('[name="mptbm_post_id"]').val(post_id); 4124 parent.find('[name="mptbm_post_id"]').attr('data-price', transport_price * quantityVal); 4125 parent.find('[name="mptbm_post_id"]').attr('data-unit-transport-price', transport_price); 4126 parent.find('[name="mptbm_post_id"]').attr('data-base-price-calculated', 0); 4127 parent.find('[name="mptbm_post_id"]').attr('data-unit-base-price', 0); 4128 4129 // --- BASE PRICE CALCULATION --- 4130 // FIX: Use the server-calculated base price directly to avoid discrepancies (1.30 difference) 4131 // The server has already calculated this using high-precision coordinates and settings. 4132 // We should trust it instead of re-calculating on the client side which might use slightly different logic/API. 4133 4134 let calcBasePrice = function (callback) { 4135 let server_base_price = parseFloat($this.attr('data-unit-base-price') || 0); 4136 callback(server_base_price); 4137 }; 4138 4139 calcBasePrice(function (base_p) { 4140 parent.find('[name="mptbm_post_id"]').attr('data-base-price-calculated', base_p * quantityVal); 4141 parent.find('[name="mptbm_post_id"]').attr('data-unit-base-price', base_p); 4142 let total_b = base_p * quantityVal; 4143 4144 // Update the new inline base price detail in summary.php 4145 let detail_container = parent.find('.mptbm_base_price_detail'); 4146 if (base_p > 0) { 4147 let b_html = '<div class="_textTheme" style="font-size: 13px; margin-top: 5px; padding-left: 25px;">' + 4148 'Base Price: ' + mp_price_format(total_b) + '</div>'; 4149 detail_container.html(b_html).show(); 4150 } else { 4151 detail_container.html('').hide(); 4152 } 4153 4154 mptbm_price_calculation(parent); 4155 }); 4156 4157 // Fetch extra services + their summary in parallel instead of 4158 // one after another - each request only needs post_id and 4159 // renders its own independent template server-side (see 4160 // get_mptbm_extra_service/_summary in MPTBM_Transport_Search.php), 4161 // so chaining them was just doubling the wait for no reason. 4162 dLoader(parent.find('.tabsContentNext')); 4163 var extraServiceReq = $.ajax({ 4164 type: 'POST', 4165 url: mp_ajax_url, 4166 data: { "action": "get_mptbm_extra_service", "nonce": mptbm_ajax.search_nonce, "post_id": post_id } 4167 }); 4168 var extraServiceSummaryReq = $.ajax({ 4169 type: 'POST', 4170 url: mp_ajax_url, 4171 data: { "action": "get_mptbm_extra_service_summary", "nonce": mptbm_ajax.search_nonce, "post_id": post_id } 4172 }); 4173 $.when(extraServiceReq, extraServiceSummaryReq).done(function (serviceResp, summaryResp) { 4174 target_extra_service.html(serviceResp[0]); 4175 // The footer copy of .mptbm_transport_summary just arrived with 4176 // this markup - CSS hides every .mptbm_transport_summary by 4177 // default (shown only via the sidebar's own slideDown, which 4178 // already ran before this element existed), so reveal this one 4179 // explicitly rather than waiting on the next price recalculation. 4180 target_extra_service.find('.mptbm_transport_summary').show(); 4181 // Fill in its price/total immediately - otherwise it stays blank 4182 // until the customer's first extra-service click, since the vehicle 4183 // price line and base price calculation both already ran before 4184 // this copy existed. Copy the sidebar's already-computed price line 4185 // rather than re-deriving it (quantity/tax/custom-message logic 4186 // lives in the click handler above, not worth duplicating here). 4187 target_extra_service.find('.mptbm_product_price').html( 4188 target_summary.find('.mptbm_product_price').first().html() 4189 ); 4190 mptbm_price_calculation(parent); 4191 checkAndToggleBookNowButton(parent); 4192 if (mptbm_is_ios()) { 4193 target_extra_service[0].style.display = 'none'; 4194 void target_extra_service[0].offsetHeight; 4195 target_extra_service[0].style.display = ''; 4196 } 4197 4198 // Re-query instead of using target_extra_service_summary as 4199 // captured before this vehicle's markup existed: that snapshot 4200 // only ever matched the sidebar copy, so the footer copy 4201 // (revealed just above) would never receive the itemized 4202 // breakdown otherwise. 4203 let all_extra_service_summary = parent.find('.mptbm_extra_service_summary'); 4204 all_extra_service_summary.html(summaryResp[0]).promise().done(function () { 4205 if (target_extra_service.find('[name="mptbm_extra_service[]"]').length > 0) { 4206 target_summary.slideDown(400); 4207 target_extra_service.slideDown(400); 4208 all_extra_service_summary.slideDown(400); 4209 pageScrollTo(target_extra_service); 4210 } 4211 dLoaderRemove(parent.find('.tabsContentNext')); 4212 if (!target_extra_service.find('[name="mptbm_extra_service[]"]').length) { 4213 parent.find('.mptbm_book_now[type="button"]').trigger('click'); 4214 } else { 4215 checkAndToggleBookNowButton(parent); 4216 } 4217 if (mptbm_is_ios()) { 4218 all_extra_service_summary.hide().show(0); 4219 } 4220 }); 4221 }); 4222 }); 4223 } 4224 }); 4225 $(document).on('click', '.mptbm_transport_search_area .mptbm_price_calculation', function () { 4226 mptbm_price_calculation($(this).closest('.mptbm_transport_search_area')); 4227 }); 4228 //========Extra service==============// 4229 $(document).on('change', '.mptbm_transport_search_area [name="mptbm_extra_service_qty[]"]', function () { 4230 $(this).closest('.mptbm_extra_service_item').find('[name="mptbm_extra_service[]"]').trigger('change'); 4231 let parent = $(this).closest('.mptbm_transport_search_area'); 4232 checkAndToggleBookNowButton(parent); 4233 }); 4234 $(document).on('change', '.mptbm_transport_search_area [name="mptbm_extra_service[]"]', function () { 4235 let parent = $(this).closest('.mptbm_transport_search_area'); 4236 let service_name = $(this).data('value'); 4237 let service_value = $(this).val(); 4238 if (service_value) { 4239 let qty = $(this).closest('.mptbm_extra_service_item').find('[name="mptbm_extra_service_qty[]"]').val(); 4240 parent.find('[data-extra-service="' + service_name + '"]').slideDown(350).find('.ex_service_qty').html('x' + qty); 4241 } else { 4242 parent.find('[data-extra-service="' + service_name + '"]').slide
4242Up(350); 4243 } 4244 mptbm_price_calculation(parent); 4245 4246 checkAndToggleBookNowButton(parent); 4247 }); 4248 4249 function checkAndToggleBookNowButton(parent) { 4250 // Check if there are any extra services present 4251 let extraServicesAvailable = parent.find('[name="mptbm_extra_service[]"]').length > 0; 4252 4253 if (extraServicesAvailable) { 4254 parent.find('.mptbm_book_now[type="button"]').show(); 4255 } else { 4256 parent.find('.mptbm_book_now[type="button"]').hide(); 4257 } 4258 } 4259 4260 4261 4262 //===========================// 4263 $(document).on('click', '.mptbm_map_collapse_toggle', function () { 4264 var $btn = $(this); 4265 var $mapArea = $btn.closest('.mptbm_map_area'); 4266 var collapsed = $mapArea.toggleClass('mptbm_map_collapsed').hasClass('mptbm_map_collapsed'); 4267 $btn.attr('aria-expanded', collapsed ? 'false' : 'true'); 4268 $btn.find('[data-label]').text(collapsed ? $btn.data('expand-text') : $btn.data('collapse-text')); 4269 mptbm_refit_osm_map(); 4270 }); 4271 4272 //===========================// 4273 $(document).on('click', '.mptbm_inline_results_reset', function () { 4274 window.location.reload(); 4275 }); 4276 4277 //===========================// 4278 $(document).on('click', '.mptbm_transport_search_area .mptbm_get_vehicle_prev', function () { 4279 var mptbmTemplateExists = $(".mptbm-show-search-result").length; 4280 if (mptbmTemplateExists) { 4281 // Function to retrieve cookie value by name 4282 function getCookie(name) { 4283 // Split the cookies by semicolon 4284 var cookies = document.cookie.split(";"); 4285 // Loop through each cookie to find the one with the specified name 4286 for (var i = 0; i < cookies.length; i++) { 4287 var cookie = cookies[i].trim(); 4288 // Check if the cookie starts with the specified name 4289 if (cookie.startsWith(name + "=")) { 4290 // Return the value of the cookie 4291 return cookie.substring(name.length + 1); 4292 } 4293 } 4294 // Return null if the cookie is not found 4295 return null; 4296 } 4297 // Usage example: 4298 var httpReferrerValue = getCookie("httpReferrer"); 4299 // Function to delete a cookie by setting its expiry date to a past time 4300 function deleteCookie(name) { 4301 document.cookie = 4302 name + "=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;"; 4303 } 4304 deleteCookie("httpReferrer"); 4305 window.location.href = httpReferrerValue; 4306 } else { 4307 let parent = $(this).closest(".mptbm_transport_search_area"); 4308 parent.find(".get_details_next_link").slideDown("fast"); 4309 parent.find(".nextTab_prev").trigger("click"); 4310 } 4311 }); 4312 $(document).on('click', '.mptbm_transport_search_area .mptbm_summary_prev', function () { 4313 let mptbmTemplateExists = $(".mptbm-show-search-result").length; 4314 if (mptbmTemplateExists) { 4315 $(".mptbm_order_summary").css("display", "none"); 4316 $(".mptbm_map_search_result").css("display", "block").hide().slideDown("slow"); 4317 $(".step-place-order").removeClass("active"); 4318 } else { 4319 let parent = $(this).closest(".mptbm_transport_search_area"); 4320 parent.find(".nextTab_prev").trigger("click"); 4321 } 4322 }); 4323 //===========================// 4324 $(document).on("click", ".mptbm_book_now[type='button']", function () { 4325 if ($(this).is(':disabled')) { 4326 // Booking is unavailable (no WooCommerce and no Pro plugin active); 4327 // the disabled state can still be reached via the auto-click that 4328 // fires when a vehicle has no extra services, so bail out here too. 4329 return; 4330 } 4331 let parent = $(this).closest('.mptbm_transport_search_area'); 4332 let target_checkout = parent.find('.mptbm_checkout_area'); 4333 let start_place = parent.find('[name="mptbm_start_place"]').val(); 4334 let end_place = parent.find('[name="mptbm_end_place"]').val(); 4335 let mptbm_waiting_time = parent.find('[name="mptbm_waiting_time"]').val(); 4336 let mptbm_taxi_return = parent.find('[name="mptbm_taxi_return"]').val(); 4337 let return_target_date = parent.find("#mptbm_map_return_date").val(); 4338 let return_target_time = parent.find("#mptbm_map_return_time").val(); 4339 let mptbm_fixed_hours = parent.find('[name="mptbm_fixed_hours"]').val(); 4340 let post_id = parent.find('[name="mptbm_post_id"]').val(); 4341 let date = parent.find('[name="mptbm_date"]').val(); 4342 let link_id = $(this).attr('data-wc_link_id'); 4343 let quantity = parseInt(parent.find(`.mp_quantity_input[data-post-id="${post_id}"]`).val()) || 1; 4344 let mptbm_original_price_base = parent.find('[name="mptbm_original_price_base"]').val(); 4345 let mptbm_threshold_base_price = parent.find('[name="mptbm_post_id"]').attr('data-base-price-calculated') || 0;
4346 // Generated with the vehicle-result response, so it remains fresh even when a 4347 // guest received the outer booking page from a full-page cache. 4348 let add_to_cart_nonce = parent.find('[name="mptbm_add_to_cart_nonce"]').val() || ''; 4349 4350 if (start_place !== '' && end_place !== '' && link_id && post_id) { 4351 let extra_service_name = {}; 4352 let extra_service_qty = {}; 4353 let count = 0; 4354 parent.find('[name="mptbm_extra_service[]"]').each(function () { 4355 let ex_name = $(this).val(); 4356 if (ex_name) { 4357 extra_service_name[count] = ex_name; 4358 let ex_qty = parseInt($(this).closest('.mptbm_extra_service_item').find('[name="mptbm_extra_service_qty[]"]').val()); 4359 ex_qty = ex_qty > 0 ? ex_qty : 1; 4360 extra_service_qty[count] = ex_qty; 4361 count++; 4362 } 4363 }); 4364 4365 // Get coordinates for fixed_zone/fixed_zone_dropoff pricing 4366 let start_place_coordinates = null; 4367 let end_place_coordinates = null; 4368 4369 // Try to get from hidden inputs first 4370 let start_coords_input = parent.find('input[name="mptbm_start_place_coordinates"]'); 4371 let end_coords_input = parent.find('input[name="mptbm_end_place_coordinates"]'); 4372 4373 if (start_coords_input.length && start_coords_input.val()) { 4374 try { 4375 start_place_coordinates = JSON.parse(start_coords_input.val()); 4376 } catch (e) { 4377 start_place_coordinates = start_coords_input.val(); 4378 } 4379 } else if (typeof window.mptbm_fixed_zone_start_coords !== 'undefined' && window.mptbm_fixed_zone_start_coords) { 4380 start_place_coordinates = window.mptbm_fixed_zone_start_coords; 4381 } else if (mptbm_start_marker) { 4382 let pos = mptbm_start_marker.getPosition(); 4383 if (pos) { 4384 start_place_coordinates = { latitude: pos.lat(), longitude: pos.lng() }; 4385 } 4386 } 4387 4388 if (end_coords_input.length && end_coords_input.val()) { 4389 try { 4390 end_place_coordinates = JSON.parse(end_coords_input.val()); 4391 } catch (e) { 4392 end_place_coordinates = end_coords_input.val(); 4393 } 4394 } else if (typeof window.mptbm_fixed_zone_end_coords !== 'undefined' && window.mptbm_fixed_zone_end_coords) { 4395 end_place_coordinates = window.mptbm_fixed_zone_end_coords; 4396 } else if (mptbm_end_marker) { 4397 let pos = mptbm_end_marker.getPosition(); 4398 if (pos) { 4399 end_place_coordinates = { latitude: pos.lat(), longitude: pos.lng() }; 4400 } 4401 } 4402 4403 $.ajax({ 4404 type: 'POST', 4405 url: mp_ajax_url, 4406 data: { 4407 action: "mptbm_add_to_cart", 4408 mptbm_add_to_cart_nonce: add_to_cart_nonce, 4409 // Kept for one rolling-deployment cycle so an older PHP handler can 4410 // still validate clients whose assets update before the backend. 4411 nonce: mptbm_ajax.search_nonce, 4412 //"product_id": post_id, 4413 transport_quantity: quantity, 4414 link_id: link_id, 4415 mptbm_start_place: start_place, 4416 mptbm_end_place: end_place, 4417 mptbm_waiting_time: mptbm_waiting_time, 4418 mptbm_taxi_return: mptbm_taxi_return, 4419 mptbm_fixed_hours: mptbm_fixed_hours, 4420 mptbm_date: date, 4421 mptbm_return_date: return_target_date, 4422 mptbm_return_time: return_target_time, 4423 mptbm_extra_service: extra_service_name, 4424 mptbm_extra_service_qty: extra_service_qty, 4425 mptbm_passengers: parent.find('#mptbm_passengers').val(), 4426 mptbm_max_passenger: parent.find('#mptbm_max_passenger').val(), 4427 mptbm_max_bag: parent.find('#mptbm_max_bag').val(), 4428 mptbm_max_hand_luggage: parent.find('#mptbm_max_hand_luggage').val(), 4429 mptbm_extra_stop_place: parent.find('input[name="mptbm_extra_stop_place"]').val(), 4430 mptbm_original_price_base: mptbm_original_price_base, 4431 mptbm_distance: parent.find('input[name="mptbm_hidden_distance"]').val(), 4432 mptbm_duration: parent.find('input[name="mptbm_hidden_duration"]').val(), 4433 mptbm_duration_text: parent.find('input[name="mptbm_hidden_duration_text"]').val(), 4434 start_place_coordinates: start_place_coordinates ? JSON.stringify(start_place_coordinates) : '', 4435 end_place_coordinates: end_place_coordinates ? JSON.stringify(end_place_coordinates) : '', 4436 mptbm_threshold_base_price: mptbm_threshold_base_price, 4437 // Standalone (no-WooCommerce) Pro custom booking flow fields. Ignored 4438 // by the WooCommerce add-to-cart handler when WooCommerce is active. 4439 mptbm_payment_method: parent.find('[name="mptbm_payment_method"]:checked').val() || '', 4440 mptbm_billing_name: parent.find('[name="mptbm_billing_name"]').val() || '', 4441 mptbm_billing_email: parent.find('[name="mptbm_billing_email"]').val() || '', 4442 mptbm_billing_phone: parent.find('[name="mptbm_billing_phone"]').val() || '' 4443 }, 4444 beforeSend: function () { 4445 dLoader(parent.find('.tabsContentNext')); 4446 }, 4447 success: function (data) { 4448 if ($('<div />', { html: data }).find("div").length > 0) {
4449 var mptbmTemplateExists = $(".mptbm-show-search-result").length; 4450 if (mptbmTemplateExists) { 4451 $(".mptbm_map_search_result").css("display", "none"); 4452 $(".mptbm_order_summary").css("display", "block"); 4453 $(".step-place-order").addClass('active'); 4454 } 4455 target_checkout.html(data).promise().done(function () { 4456 target_checkout.find('.woocommerce-billing-fields .required').each(function () { 4457 $(this).closest('p').find('.input-text , select, textarea ').attr('required', 'required'); 4458 }); 4459 $(document.body).trigger('init_checkout'); 4460 if ($('body select#billing_country').length > 0) { 4461 $('body select#billing_country').select2({}); 4462 } 4463 if ($('body select#billing_state').length > 0) { 4464 $('body select#billing_state').select2({}); 4465 } 4466 dLoaderRemove(parent.find('.tabsContentNext')); 4467 parent.find('.nextTab_next').trigger('click'); 4468 // iOS DOM reflow workaround 4469 if (mptbm_is_ios()) { 4470 target_checkout[0].style.display = 'none'; 4471 void target_checkout[0].offsetHeight; 4472 target_checkout[0].style.display = ''; 4473 } 4474 }); 4475 } else if (data && /^https?:\/\//i.test(data.trim())) { 4476 window.location.href = data; 4477 } else { 4478 // Empty/invalid response (e.g. both WooCommerce and a 4479 // custom payment method were enabled and the request 4480 // fell through without a usable result) - clear the 4481 // loader instead of leaving the button stuck. 4482 dLoaderRemove(parent.find('.tabsContentNext')); 4483 console.log('mptbm_add_to_cart: unexpected response', data); 4484 } 4485 }, 4486 error: function (response) { 4487 dLoaderRemove(parent.find('.tabsContentNext')); 4488 console.log(response); 4489 } 4490 }); 4491 } else { 4492 // Missing required data - bail out loud instead of leaving the button 4493 // looking clicked with no visible feedback and nothing in the console. 4494 console.warn('mptbm_book_now: missing required booking data, not submitting', { 4495 start_place: start_place, end_place: end_place, link_id: link_id, post_id: post_id 4496 }); 4497 } 4498 }); 4499 4500 4501 4502 $(document).ready(function () { 4503 let $tabs = $('.tab-link'); 4504 let count = $tabs.length; 4505 4506 // Reset previous border-radius styles 4507 $tabs.css({ 4508 'border-radius': '', // Clears any previously applied styles 4509 }); 4510 4511 if (count === 1) { 4512 // If only one element, apply radius to all sides 4513 $tabs.eq(0).css('border-radius', 'var(--dbrl)'); 4514 } else if (count >= 2) { 4515 // If three or more, apply left radius to first and right radius to third 4516 /*$tabs.eq(0).css({ 4517 'border-top-left-radius': 'var(--dbrl)', 4518 'border-bottom-left-radius': 'var(--dbrl)' 4519 }); 4520 $tabs.last().css({ 4521 'border-top-right-radius': 'var(--dbrl)', 4522 'border-bottom-right-radius': 'var(--dbrl)' 4523 });*/ 4524 } 4525 $('.mptb-tabs li').click(function () { 4526 var tab_id = $(this).attr('mptbm-data-tab'); 4527 var form_style = $(this).attr('mptbm-data-form-style'); 4528 var map = $(this).attr('mptbm-data-map'); 4529 var $tabContentWrap = $(this).closest('.mptb-tab-container').find('.mptb-tabs-content-wrap'); 4530 var vehicle_id = parseInt($(this).closest('.mptbm_transport_search_area').find('[name="mptbm_source_vehicle_id"]').val(), 10) || 0; 4531 4532 // Ignore re-clicks on the active tab or while a switch is already loading 4533 if ($(this).hasClass('current') || $tabContentWrap.hasClass('mptbm-tab-loading')) { 4534 return; 4535 } 4536 4537 // Clean up existing map instance before switching tabs 4538 mptbm_cleanup_map(); 4539 4540 // Freeze height so the wrap does not collapse while content stays 4541 // visible (faded) under the spinner. After load, settleHeight() 4542 // eases to the new content size, then clears the lock (height: auto). 4543 var lockedHeight = $tabContentWrap.outerHeight() || 0; 4544 if (lockedHeight > 0) { 4545 $tabContentWrap.css('min-height', lockedHeight + 'px'); 4546 } 4547 4548 function settleHeight() { 4549 if (!$tabContentWrap.length) { 4550 return; 4551 } 4552 var $currentTab = $tabContentWrap.find('.mptb-tab-content.current'); 4553 var contentHeight = $currentTab.length ? $currentTab.outerHeight(true) : 0; 4554 if (contentHeight > 0) { 4555 $tabContentWrap.css('min-height', contentHeight + 'px'); 4556 } 4557 setTimeout(function () { 4558 $tabContentWrap.css('min-height', ''); 4559 }, 260); 4560 } 4561 4562 function clearTabLoader() { 4563 $tabContentWrap.removeClass('mptbm-tab-loading'); 4564 $tabContentWrap.find('.mptbm-tab-loader, .mptbm-loading-overlay').remove(); 4565 } 4566 4567 // Keep the active tab content on screen (opacity via CSS) and show 4568 // a spinner with no overlay background while the next tab loads. 4569 clearTabLoader(); 4570 $tabContentWrap.addClass('mptbm-tab-loading'); 4571 $tabContentWrap.append('<div class="mptbm-tab-loader"><div class="mptbm-spinner"></div></div>'); 4572 4573 // Mark the clicked tab pill as active 4574 $('.mptb-tabs li').removeClass('current'); 4575 $(this).addClass('current'); 4576 4577 $.ajax({ 4578 type: "POST", 4579 url: mp_ajax_url, 4580 data: { 4581 action: "load_get_details_page", 4582 nonce: mptbm_ajax.search_nonce, 4583 tab_id: tab_id, 4584 form_style: form_style, 4585 map: map, 4586 vehicle_id: vehicle_id 4587 }, 4588 success: function (data) { 4589 var tabContainer = $("#" + tab_id); 4590 if (tabContainer.length === 0) { 4591 var tabContainerParent = $tabContentWrap.length ? $tabContentWrap : $('.mptb-tab-container'); 4592 if (tabContainerParent.length > 0) {
4593 var newTabContainer = $('<div id="' + tab_id + '" class="mptb-tab-content"></div>'); 4594 tabContainerParent.append(newTabContainer); 4595 tabContainer = newTabContainer; 4596 } else { 4597 console.error('Tab container parent not found'); 4598 clearTabLoader(); 4599 return; 4600 } 4601 } 4602 4603 // Empty other tabs only now â keeps previous content visible 4604 // under the faded loader until replacement is ready, and 4605 // avoids duplicate field IDs once the new markup is inserted. 4606 $('.mptb-tab-content').not(tabContainer).empty(); 4607 tabContainer.html(data); 4608 4609 $('.mptb-tab-content').removeClass('current'); 4610 tabContainer.addClass('current'); 4611 4612 if (!tabContainer.is(':visible')) { 4613 tabContainer.css('display', 'block'); 4614 } 4615 4616 clearTabLoader(); 4617 settleHeight(); 4618 4619 setTimeout(function () { 4620 var currentTab = $('.mptb-tabs li.current').attr('mptbm-data-tab'); 4621 var mapEnabled = $('.mptb-tabs li.current').attr('mptbm-data-map'); 4622 4623 if (mapEnabled === 'yes' && mptbm_get_current_map_wrap() && mptbm_get_current_map_wrap().style.display !== 'none') { 4624 mptbm_map_area_init(); 4625 } 4626 4627 var mapType = document.getElementById('mptbm_map_type'); 4628 if (!(mapType && mapType.value === 'openstreetmap')) { 4629 initializeGooglePlacesAutocomplete(); 4630 } 4631 4632 settleHeight(); 4633 }, 100); 4634 }, 4635 error: function () { 4636 clearTabLoader(); 4637 var tabContainer = $("#" + tab_id); 4638 if (tabContainer.length > 0) { 4639 $('.mptb-tab-content').not(tabContainer).empty(); 4640 tabContainer.html('<div style="text-align: center; padding: 20px; color: red;"><p>Error loading content. Please try again.</p></div>'); 4641 $('.mptb-tab-content').removeClass('current'); 4642 tabContainer.addClass('current'); 4643 } 4644 settleHeight(); 4645 }, 4646 }); 4647 }); 4648 }); 4649 4650 // Handle select dropdown search functionality 4651 $(document).on('click', '#mptbm_manual_start_place, #mptbm_manual_end_place', function (e) { 4652 4653 4654 var $select = $(this); 4655 var selectId = $select.attr('id'); 4656 4657 4658 // Remove any existing custom search elements 4659 $('.mptbm-custom-select-wrapper').remove(); 4660 4661 // Check if select has options (dropoff might be empty initially) 4662 var $options = $select.find('option:not([disabled])'); 4663 4664 if ($options.length <= 0) { 4665 return; 4666 } 4667 4668 // Get select position and dimensions 4669 var selectOffset = $select.offset(); 4670 var selectWidth = $select.outerWidth(); 4671 var selectHeight = $select.outerHeight(); 4672 4673 // Keep the original select visible - don't hide it 4674 // $select.hide(); // REMOVED - keep select visible 4675 4676 // Create custom select wrapper with dynamic positioning 4677 // (cosmetic styling lives in mptbm_registration.css; only positioning is set inline) 4678 var $customWrapper = $('<div class="mptbm-custom-select-wrapper"></div>'); 4679 4680 // Create search input 4681 var $searchInput = $('<input type="text" class="mptbm-custom-search-input" placeholder="Search locations..." />'); 4682 4683 // Create options container
4684 var $optionsContainer = $('<div class="mptbm-custom-options"></div>'); 4685 4686 // Function to update dropdown position 4687 function updateDropdownPosition() { 4688 var currentOffset = $select.offset(); 4689 var currentWidth = $select.outerWidth(); 4690 var currentHeight = $select.outerHeight(); 4691 4692 var scrollTop = $(window).scrollTop(); 4693 var scrollLeft = $(window).scrollLeft(); 4694 4695 // Always position below 4696 var top = currentOffset.top - scrollTop + currentHeight + 2; 4697 var left = currentOffset.left - scrollLeft; 4698 var width = currentWidth; 4699 4700 var windowHeight = $(window).height(); 4701 var windowWidth = $(window).width(); 4702 4703 // Calculate available space and adjust dropdown height accordingly 4704 var availableHeight = windowHeight - top - 20; // 20px padding from bottom 4705 var maxHeight = Math.min(250, Math.max(100, availableHeight)); // Min 100px, max 250px 4706 4707 $optionsContainer.css('max-height', maxHeight + 'px'); 4708 4709 // Prevent offscreen left/right 4710 if (left + width > windowWidth - 10) { 4711 left = windowWidth - width - 10; 4712 } 4713 if (left < 10) left = 10; 4714 4715 $customWrapper.css({ 4716 top: top + 'px', 4717 left: left + 'px', 4718 width: width + 'px' 4719 }); 4720 } 4721 4722 // Get all options from original select (excluding disabled ones) 4723 var $originalOptions = $select.find('option:not([disabled])'); 4724 var optionsHtml = ''; 4725 4726 4727 $originalOptions.each(function () { 4728 var optionText = $(this).text(); 4729 var optionValue = $(this).val(); 4730 var isSelected = $(this).is(':selected'); 4731 4732 var selectedClass = isSelected ? 'mptbm-option-selected' : ''; 4733 optionsHtml += '<div class="mptbm-custom-option ' + selectedClass + '" data-value="' + optionValue + '">' + optionText + '</div>'; 4734 }); 4735 4736 $optionsContainer.html(optionsHtml); 4737 4738 // Assemble and append to mptbm_transport_search_area 4739 $customWrapper.append($searchInput).append($optionsContainer); 4740 $('.mptbm_transport_search_area').append($customWrapper); 4741 4742 // Initial and responsive position 4743 updateDropdownPosition(); 4744 $(window).on('scroll resize', updateDropdownPosition); 4745 4746 // Ensure map elements are not affected by the dropdown 4747 $('.mptbm_map_area').css('z-index', '1'); 4748 $('.mptbm_map_area #mptbm_map_area').css('z-index', '1'); 4749 4750 // Focus on search input 4751 $searchInput.focus(); 4752 4753 // Handle search input 4754 $searchInput.on('input', function () { 4755 var searchTerm = $(this).val().toLowerCase(); 4756 var $options = $customWrapper.find('.mptbm-custom-option'); 4757 4758 4759 $options.each(function () { 4760 var optionText = $(this).text().toLowerCase(); 4761 if (optionText.includes(searchTerm) || searchTerm === '') { 4762 $(this).show(); 4763 } else { 4764 $(this).hide(); 4765 } 4766 }); 4767 }); 4768 4769 // Handle option selection 4770 $customWrapper.on('click', '.mptbm-custom-option', function () { 4771 var selectedValue = $(this).data('value'); 4772 var selectedText = $(this).text(); 4773 4774 // Update original select 4775 $select.val(selectedValue); 4776 $select.trigger('change'); 4777 4778 // Update search input with selected text 4779 $searchInput.val(selectedText); 4780 4781 // Remove custom wrapper (select stays visible) 4782 $customWrapper.remove(); 4783 4784 // Restore map z-index 4785 $('.mptbm_map_area').css('z-index', ''); 4786 $('.mptbm_map_area #mptbm_map_area').css('z-index', ''); 4787 4788 }); 4789 4790 // Handle select change event to clean up custom dropdown 4791 $select.one('change', function () { 4792 $customWrapper.remove(); 4793 // Restore map z-index 4794 $('.mptbm_map_area').css('z-index', ''); 4795 $('.mptbm_map_area #mptbm_map_area').css('z-index', ''); 4796 }); 4797 4798 // Handle clicking outside to close 4799 $(document).one('click', function (e) { 4800 if (!$(e.target).closest('.mptbm-custom-select-wrapper, #' + selectId).length) { 4801 $customWrapper.remove(); 4802 // Restore map z-index 4803 $('.mptbm_map_area').css('z-index', ''); 4804 $('.mptbm_map_area #mptbm_map_area').css('z-index', ''); 4805 } 4806 }); 4807 4808 // Handle window resize to update dropdown position with debouncing 4809 var positionUpdateTimeout; 4810 var positionUpdateHandler = function () { 4811 clearTimeout(positionUpdateTimeout); 4812 positionUpdateTimeout = setTimeout(function () { 4813 updateDropdownPosition(); 4814 }, 16); // ~60fps throttling 4815 }; 4816 4817 $(window).on('resize.mptbm-dropdown', positionUpdateHandler); 4818 4819 // Clean up event listeners when dropdown is removed 4820 var originalRemove = $customWrapper.remove; 4821 $customWrapper.remove = function () { 4822 clearTimeout(positionUpdateTimeout); 4823 $(window).off('resize.mptbm-dropdown'); 4824 $(window).off('scroll resize', updateDropdownPosition); 4825 return originalRemove.call(this); 4826 }; 4827 4828 // Handle escape key 4829 $searchInput.on('keydown', function (e) { 4830 if (e.key === 'Escape') { 4831 $customWrapper.remove(); 4832 // Restore map z-index 4833 $('.mptbm_map_area').css('z-index', ''); 4834 $('.mptbm_map_area #mptbm_map_area').css('z-index', ''); 4835 } 4836 }); 4837 }); 4838 4839 // Prevent native dropdown behavior for manual select elements 4840 $(document).on('focus mousedown keydown', '#mptbm_manual_start_place, #mptbm_manual_end_place', function (e) { 4841 // Only prevent if it's not already handled by our custom dropdown 4842 if (!$(e.target).closest('.mptbm-custom-select-wrapper').length) { 4843 if (e.type === 'focus' || e.type === 'mousedown' || 4844 (e.type === 'keydown' && (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown' || e.key === 'ArrowUp'))) { 4845 e.preventDefault(); 4846 e.stopPropagation(); 4847 } 4848 } 4849 }); 4850 4851 // "View Details" toggle - expands the real specs/reviews panel rendered 4852 // server-side in vehicle_item.php (only present when there is genuinely 4853 // more content than the visible feature chips). 4854 $(document).on('click', '.mptbm_view_details_toggle', function (e) { 4855 e.preventDefault(); 4856 e.stopPropagation(); 4857 4858 var $button = $(this); 4859 var postId = $button.data('post-id'); 4860 var $wrapper = $button.closest('.mptbm-vehicle-wrapper'); 4861 var $panel = $wrapper.find('.mptbm_vehicle_details_panel[data-post-id="' + postId + '"]'); 4862 if (!$panel.length) { 4863 return; 4864 } 4865 4866 $panel.slideToggle(250, function () { 4867 var isOpen = $panel.is(':visible'); 4868 $button.attr('aria-expanded', isOpen ? 'true' : 'false'); 4869 $button.toggleClass('is-open', isOpen); 4870 $button.find('[data-label]').text(isOpen ? $button.data('hide-text') : $button.data('view-text')); 4871 }); 4872 }); 4873 4874}(jQuery)); 4875 4876function gm_authFailure() { 4877 var warning = jQuery('.mptbm-map-warning').html(); 4878 jQuery('#mptbm_map_area').html('<div class="mptbm-map-warning"><h6>' + warning + '</h6></div>'); 4879} 4880// Utility: Detect iOS 4881function mptbm_is_ios() { 4882 return /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream; 4883} 4884 4885// Utility: Detect Safari 4886function mptbm_is_safari() { 4887 return /^((?!chrome|android).)*safari/i.test(navigator.userAgent); 4888} 4889 4890// Fallback distance calculation for Safari when Google Maps API fails 4891function mptbm_fallback_distance_calculation(start_place, end_place) { 4892 4893 // Simple fallback: show placeholder values 4894 var fallback_distance = "Calculating..."; 4895 var fallback_duration = "Calculating..."; 4896 4897 // Update UI with fallback values 4898 jQuery(".mptbm_total_distance").html(fallback_distance); 4899 jQuery(".mptbm_total_time").html(fallback_duration); 4900 jQuery(".mptbm_distance_time").slideDown("fast"); 4901 4902 // Set cookies with fallback values 4903 var now = new Date(); 4904 var time = now.getTime(); 4905 var expireTime = time + 3600 * 1000 * 12; 4906 now.setTime(expireTime); 4907 4908 var cookieOptions = "; expires=" + now.toUTCString() + "; path=/; SameSite=Lax"; 4909 document.cookie = "mptbm_distance=" + encodeURIComponent("0") + cookieOptions; 4910 document.cookie = "mptbm_distance_text=" + encodeURIComponent(fallback_distance) + cookieOptions; 4911 document.cookie = "mptbm_duration=" + encodeURIComponent("0") + cookieOptions; 4912 document.cookie = "mptbm_duration_text=" + encodeURIComponent(fallback_duration) + cookieOptions; 4913 4914 // Try to use server-side calculation as backup 4915 if (typeof mp_ajax_url !== 'undefined') { 4916 console.log("MPTBM JS: Calling server-side fallback"); 4917 jQuery.ajax({ 4918 type: "POST", 4919 url: mp_ajax_url, 4920 data: { 4921 action: "mptbm_calculate_distance_fallback", 4922 start_place: start_place, 4923 end_place: end_place 4924 }, 4925 success: function (response) { 4926 console.log("MPTBM JS: Server fallback response", response); 4927 if (response.success && response.data) { 4928 var distElem = jQuery(".mptbm_total_distance"); 4929 var timeElem = jQuery(".mptbm_total_time"); 4930 console.log("MPTBM JS: Updating UI. Dist Elem length:", distElem.length, "Time Elem length:", timeElem.length); 4931 4932 distElem.html(response.data.distance_text); 4933 timeElem.html(response.data.duration_text); 4934
4935 // Update hidden inputs for AJAX fallback 4936 var mapArea = jQuery('#mptbm_map_area').closest('.mptbm_transport_search_area'); 4937 if (mapArea.length > 0) { 4938 mapArea.find('input[name="mptbm_hidden_distance"]').val(response.data.distance); 4939 mapArea.find('input[name="mptbm_hidden_duration"]').val(response.data.duration); 4940 mapArea.find('input[name="mptbm_hidden_distance_text"]').val(response.data.distance_text); 4941 mapArea.find('input[name="mptbm_hidden_duration_text"]').val(response.data.duration_text); 4942 } 4943 4944 // Update cookies with server response 4945 document.cookie = "mptbm_distance=" + encodeURIComponent(response.data.distance) + cookieOptions; 4946 document.cookie = "mptbm_distance_text=" + encodeURIComponent(response.data.distance_text) + cookieOptions; 4947 document.cookie = "mptbm_duration=" + encodeURIComponent(response.data.duration) + cookieOptions; 4948 document.cookie = "mptbm_duration_text=" + encodeURIComponent(response.data.duration_text) + cookieOptions; 4949 } 4950 }, 4951 error: function () { 4952 console.log("Server-side distance calculation also failed"); 4953 } 4954 }); 4955 } 4956} 4957 4958// "Best Price" badge on the search-results vehicle cards: computed from the 4959// actual prices rendered for the current search (not a fabricated claim). 4960// The vehicle list loads in over AJAX after a search, and .mainSection 4961// itself doesn't exist until then, so this watches the whole document for 4962// it to appear/change rather than trying to bind to a specific element. 4963(function ($) { 4964 "use strict"; 4965 4966 function highlightBestPrice() { 4967 $('.mainSection').each(function () { 4968 var mainSection = $(this); 4969 var cheapestBtn = null; 4970 var cheapestPrice = Infinity; 4971 4972 mainSection.find('.mptbm_transport_select[data-transport-price]').each(function () { 4973 var price = parseFloat($(this).attr('data-transport-price')); 4974 var item = $(this).closest('.mptbm_booking_item'); 4975 if (!price || price <= 0 || item.hasClass('mptbm_booking_item_hidden')) { 4976 return; 4977 } 4978 if (price < cheapestPrice) { 4979 cheapestPrice = price; 4980 cheapestBtn = $(this); 4981 } 4982 }); 4983 4984 mainSection.find('.mptbm_best_price_badge').remove(); 4985 4986 if (cheapestBtn) { 4987 var image = cheapestBtn.closest('.mptbm_booking_item').find('.mptbm_vehicle_image').first(); 4988 image.append('<span class="mptbm_best_price_badge">Best Price</span>'); 4989 } 4990 }); 4991 } 4992 4993 // Results toolbar: live count of vehicles actually visible after geo-fence / 4994 // availability filtering (real data â same "hidden" class the rest of the 4995 // page already relies on), not the raw server-side query count. 4996 function updateResultsCount() { 4997 $('.mainSection').each(function () { 4998 var mainSection = $(this); 4999 var visible = mainSection.find('.mptbm_booking_item').not('.mptbm_booking_item_hidden').length; 5000 mainSection.find('.mptbm_results_count_number').text(visible); 5001 }); 5002 } 5003 5004 var debounceTimer = null; 5005 function scheduleHighlight() { 5006 clearTimeout(debounceTimer); 5007 debounceTimer = setTimeout(function () { 5008 highlightBestPrice(); 5009 updateResultsCount(); 5010 }, 150); 5011 } 5012 5013 // Sort vehicle cards by their real rendered price. "Recommended" restores 5014 // the original server-rendered order (first time we see a given results 5015 // list, its current DOM order is cached as that baseline). 5016 function sortVehicles(mainSection, mode) { 5017 var area = mainSection.find('.mp_sticky_depend_area').first(); 5018 var wrappers = area.children('.mptbm-vehicle-wrapper').toArray(); 5019 if (!wrappers.length) { 5020 return; 5021 } 5022 if (!area.data('mptbmOriginalOrder')) { 5023 area.data('mptbmOriginalOrder', wrappers.slice()); 5024 } 5025 5026 var ordered; 5027 if (mode === 'price_low' || mode === 'price_high') { 5028 ordered = wrappers.slice().sort(function (a, b) { 5029 var priceA = parseFloat($(a).find('[data-transport-price]').first().attr('data-transport-price')) || 0; 5030 var priceB = parseFloat($(b).find('[data-transport-price]').first().attr('data-transport-price')) || 0; 5031 return mode === 'price_high' ? priceB - priceA : priceA - priceB; 5032 }); 5033 } else if (mode === 'rating') { 5034 ordered = wrappers.slice().sort(function (a, b) { 5035 var ratingA = parseFloat($(a).find('[data-transport-rating]').first().attr('data-transport-rating')) || 0; 5036 var ratingB = parseFloat($(b).find('[data-transport-rating]').first().attr('data-transport-rating')) || 0; 5037 return ratingB - ratingA; 5038 }); 5039 } else { 5040 ordered = area.data('mptbmOriginalOrder'); 5041 } 5042 5043 var anchor = area.find('.geo-fence-no-transport').first(); 5044 ordered.forEach(function (el) { 5045 if (anchor.length) { 5046 $(el).insertBefore(anchor); 5047 } else { 5048 area.append(el); 5049 } 5050 }); 5051 } 5052 5053 $(document).ready(function () { 5054 highlightBestPrice(); 5055 updateResultsCount(); 5056 if (typeof MutationObserver !== 'undefined') { 5057 new MutationObserver(scheduleHighlight).observe(document.body, { childList: true, subtree: true }); 5058 } 5059 }); 5060 5061 $(document).on('change', '.mptbm_sort_select', function () { 5062 var mainSection = $(this).closest('.mainSection'); 5063 sortVehicles(mainSection, $(this).val()); 5064 }); 5065 5066 // Grid/List view toggle - purely a layout reflow of the same real cards, 5067 // no data changes. 5068 $(document).on('click', '.mptbm_results_view_toggle button', function () { 5069 var $btn = $(this); 5070 if ($btn.hasClass('is-active')) { 5071 return; 5072 } 5073 var view = $btn.data('view'); 5074 var area = $btn.closest('.mainSection').find('.mp_sticky_depend_area').first(); 5075 $btn.siblings().removeClass('is-active'); 5076 $btn.addClass('is-active'); 5077 area.toggleClass('mptbm_view_grid', view === 'grid'); 5078 area.toggleClass('mptbm_view_list', view === 'list'); 5079 }); 5080})(jQuery); 5081 5082// Multi-row "Add Extra Stop" (.mptbm_extra_stops_wrapper in get_details.php). 5083// This markup had no JS behind it at all - clicking "Add Extra Stop" did
5084// nothing. Kept self-contained (own OSM search, not the shared 5085// mptbm_setup_osm_autocomplete/mptbm_handle_osm_address_selection pair) since 5086// those are hard-wired to the single start/end/legacy-extra marker slots and 5087// reworking them for an arbitrary number of rows risked regressing the 5088// pickup/dropoff autocomplete that already works. 5089(function ($) { 5090 "use strict"; 5091 5092 // Both mptbm_set_cookie_distance_duration (Google) and 5093 // mptbm_calculate_osm_distance (OSM) now read every current 5094 // .mptbm_extra_stop_place_input / .mptbm_extra_stop_coords fresh from the 5095 // DOM on each call - this just needs to trigger the right one whenever a 5096 // stop is picked or removed, same as the existing start/end change handlers do. 5097 function mptbmExtraStopTriggerRecalculation() { 5098 var mapTypeEl = document.getElementById('mptbm_map_type'); 5099 if (mapTypeEl && mapTypeEl.value === 'openstreetmap') { 5100 if (typeof mptbm_calculate_osm_distance === 'function') { 5101 mptbm_calculate_osm_distance(); 5102 } 5103 return; 5104 } 5105 var startInput = document.getElementById('mptbm_map_start_place') || document.getElementById('mptbm_manual_start_place'); 5106 var endInput = document.getElementById('mptbm_map_end_place') || document.getElementById('mptbm_manual_end_place'); 5107 if (typeof mptbm_set_cookie_distance_duration === 'function') { 5108 mptbm_set_cookie_distance_duration(startInput ? startInput.value : '', endInput ? endInput.value : ''); 5109 } 5110 } 5111 5112 function mptbmExtraStopMaxRows($wrapper) { 5113 var max = parseInt($wrapper.attr('data-max-stops'), 10); 5114 return max > 0 ? max : 3; 5115 } 5116 5117 function mptbmExtraStopToggleAddLink($wrapper) { 5118 var max = mptbmExtraStopMaxRows($wrapper); 5119 var count = $wrapper.find('.mptbm_extra_stops_list').children('.mptbm_extra_stop_row').length; 5120 $wrapper.find('.mptbm_add_extra_stop_row').toggle(count < max); 5121 } 5122 5123 function mptbmExtraStopSearch(query, container, input, coordsInput) { 5124 container.innerHTML = '<div style="padding: 9px 12px; color:#94a3b8; font-size:13px;">Searching…</div>'; 5125 container.style.display = 'block'; 5126 5127 var body = new URLSearchParams(); 5128 body.append('action', 'mptbm_osm_search'); 5129 body.append('nonce', mptbm_ajax.osm_nonce); 5130 body.append('q', query); 5131 5132 fetch(mptbm_ajax.ajax_url, { 5133 method: 'POST', 5134 headers: { 5135 'Content-Type': 'application/x-www-form-urlencoded', 5136 'X-Requested-With': 'XMLHttpRequest' 5137 }, 5138 body: body, 5139 credentials: 'same-origin' 5140 }) 5141 .then(function (response) { return response.json(); }) 5142 .then(function (response) { 5143 // A slower-to-resolve older search (e.g. from a stop row the 5144 // customer has since edited or moved on from) must not pop 5145 // this dropdown back open with stale results out from under 5146 // whatever they're doing now - bail if the field no longer 5147 // holds the text this response was for. 5148 if (input.value.trim() !== query) { 5149 return; 5150 } 5151 5152 container.innerHTML = ''; 5153 5154 if (!response.success || !response.data || !response.data.length) { 5155 container.innerHTML = '<div style="padding: 9px 12px; color:#94a3b8; font-size:13px;">No results found</div>'; 5156 container.style.display = 'block'; 5157 return; 5158 } 5159 5160 response.data.forEach(function (result) { 5161 var item = document.createElement('div'); 5162 item.style.cssText = 'display:flex; align-items:flex-start; gap:10px; padding:10px 12px; margin:2px 0; cursor:pointer; border-radius:10px; color:#0f172a; font-size:13.5px; font-weight:500; line-height:1.4;'; 5163 5164 var icon = document.createElement('i'); 5165 icon.className = 'fas fa-map-marker-alt'; 5166 icon.style.cssText = 'flex:0 0 auto; width:14px; margin-top:2px; color:#94a3b8; font-size:13px;'; 5167 5168 var text = document.createElement('span'); 5169 text.textContent = result.display_name; 5170 text.style.cssText = 'flex:1 1 auto; min-width:0;'; 5171 5172 item.appendChild(icon); 5173 item.appendChild(text); 5174 5175 item.addEventListener('click', function () { 5176 input.value = result.display_name; 5177 // Matches the "lat,lng" string format 5178 // get_server_distance_with_stops() parses from 5179 // mptbm_extra_stop_place_coordinates[]. 5180 coordsInput.value = parseFloat(result.lat) + ',' + parseFloat(result.lon); 5181 container.style.display = 'none'; 5182 5183 // Drop/replace a pin for this specific row so the map 5184 // shows the stop, not just the bent route line. 5185 if (typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map && typeof L !== 'undefined') { 5186 if (input._mptbmStopMarker) { 5187 mptbm_osm_map.removeLayer(input._mptbmStopMarker); 5188 } 5189 input._mptbmStopMarker = L.marker([parseFloat(result.lat), parseFloat(result.lon)], { 5190 title: result.display_name 5191 }).addTo(mptbm_osm_map).bindPopup(result.display_name); 5192 } 5193 5194 mptbmExtraStopTriggerRecalculation(); 5195 }); 5196 item.addEventListener('mouseenter', function () { this.style.backgroundColor = '#f1f4f9'; }); 5197 item.addEventListener('mouseleave', function () { this.style.backgroundColor = ''; }); 5198 5199 container.appendChild(item); 5200 }); 5201 }) 5202 .catch(function () { 5203 if (input.value.trim() !== query) { 5204 return; 5205 } 5206 container.innerHTML = '<div style="padding: 9px 12px; color:#dc2626; font-size:13px;">Search failed</div>'; 5207 }); 5208 } 5209 5210 function mptbmExtraStopSetupAutocomplete($row) { 5211 var input = $row.find('.mptbm_extra_stop_place_input')[0]; 5212 var coordsInput = $row.find('.mptbm_extra_stop_coords')[0]; 5213 if (!input || !coordsInput) { 5214 return; 5215 } 5216 5217 var mapTypeEl = document.getElementById('mptbm_map_type'); 5218 var isOSM = !mapTypeEl || mapTypeEl.value === 'openstreetmap'; 5219
5220 if (!isOSM) { 5221 if (typeof google === 'undefined' || !google.maps || !google.maps.places) { 5222 return; 5223 } 5224 var autocomplete = new google.maps.places.Autocomplete(input); 5225 var restrict = $('[name="mptbm_restrict_search_country"]').val(); 5226 var country = $('[name="mptbm_country"]').val(); 5227 if (restrict === 'yes' && country) { 5228 autocomplete.setComponentRestrictions({ country: [country] }); 5229 } 5230 google.maps.event.addListener(autocomplete, 'place_changed', function () { 5231 var place = autocomplete.getPlace(); 5232 if (place && place.geometry && place.geometry.location) { 5233 coordsInput.value = place.geometry.location.lat() + ',' + place.geometry.location.lng(); 5234 } else { 5235 coordsInput.value = ''; 5236 } 5237 mptbmExtraStopTriggerRecalculation(); 5238 }); 5239 return; 5240 } 5241 5242 var resultsContainer = document.createElement('div'); 5243 resultsContainer.className = 'mptbm-osm-autocomplete'; 5244 resultsContainer.style.cssText = 'position: fixed; box-sizing: border-box; font-size:14px; background: #fff; border: 1px solid #e7eaf0; border-radius: 14px; max-height: 240px; overflow-y: auto; z-index: 99999 !important; display: none; box-shadow: 0 10px 30px rgba(15, 23, 42, 0.12), 0 2px 8px rgba(15, 23, 42, 0.06); padding: 6px;'; 5245 document.body.appendChild(resultsContainer); 5246 // Appended to body (so it can float over everything), not to the row 5247 // itself - stash a reference so removing the row can clean this up 5248 // too, instead of leaving an orphaned dropdown behind forever. 5249 input._mptbmStopResultsContainer = resultsContainer; 5250 5251 function positionDropdown() { 5252 var rect = input.getBoundingClientRect(); 5253 resultsContainer.style.top = (rect.bottom + 2) + 'px'; 5254 resultsContainer.style.left = rect.left + 'px'; 5255 resultsContainer.style.width = rect.width + 'px'; 5256 } 5257 5258 var debounceTimer = null; 5259 input.addEventListener('input', function (e) { 5260 clearTimeout(debounceTimer); 5261 var query = e.target.value.trim(); 5262 coordsInput.value = ''; 5263 if (query.length < 3) { 5264 resultsContainer.style.display = 'none'; 5265 return; 5266 } 5267 debounceTimer = setTimeout(function () { 5268 positionDropdown(); 5269 mptbmExtraStopSearch(query, resultsContainer, input, coordsInput); 5270 }, 300); 5271 }); 5272 5273 window.addEventListener('scroll', positionDropdown); 5274 window.addEventListener('resize', positionDropdown); 5275 document.addEventListener('click', function (e) { 5276 if (e.target !== input && !resultsContainer.contains(e.target)) { 5277 resultsContainer.style.display = 'none'; 5278 } 5279 }); 5280 } 5281 5282 $(document).on('click', '.mptbm_add_extra_stop_row', function () { 5283 var $wrapper = $(this).closest('.mptbm_extra_stops_wrapper'); 5284 var $list = $wrapper.find('.mptbm_extra_stops_list'); 5285 5286 if ($list.children('.mptbm_extra_stop_row').length >= mptbmExtraStopMaxRows($wrapper)) { 5287 return; 5288 } 5289 5290 var template = $wrapper.find('#mptbm_extra_stop_row_template')[0]; 5291 if (!template || !template.content) { 5292 return; 5293 } 5294 5295 $list.append(template.content.cloneNode(true)); 5296 mptbmExtraStopSetupAutocomplete($list.children('.mptbm_extra_stop_row').last()); 5297 mptbmExtraStopToggleAddLink($wrapper); 5298 }); 5299 5300 $(document).on('click', '.mptbm_remove_extra_stop_row', function () { 5301 var $wrapper = $(this).closest('.mptbm_extra_stops_wrapper'); 5302 var $row = $(this).closest('.mptbm_extra_stop_row'); 5303 var input = $row.find('.mptbm_extra_stop_place_input')[0]; 5304 var hadCoords = !!$row.find('.mptbm_extra_stop_coords').val(); 5305 if (input && input._mptbmStopMarker && typeof mptbm_osm_map !== 'undefined' && mptbm_osm_map) { 5306 mptbm_osm_map.removeLayer(input._mptbmStopMarker); 5307 } 5308 if (input && input._mptbmStopResultsContainer && input._mptbmStopResultsContainer.parentNode) { 5309 input._mptbmStopResultsContainer.parentNode.removeChild(input._mptbmStopResultsContainer); 5310 } 5311 $row.remove(); 5312 mptbmExtraStopToggleAddLink($wrapper); 5313 if (hadCoords) { 5314 mptbmExtraStopTriggerRecalculation(); 5315 } 5316 }); 5317})(jQuery);
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.