1/** 2 * @typedef {Object} Location 3 * @property {string} title - Location title 4 * @property {number} lat - Latitude 5 * @property {number} lng - Longitude 6 * @property {string} content - Location content/description 7 * @property {string} icon - URL to marker icon 8 * @property {string} post_id - WordPress post ID 9 * @property {string[]} types - Array of location types/categories 10 */ 11 12/** 13 * @typedef {Object} MapBounds 14 * @property {number} lat - Center latitude 15 * @property {number} lng - Center longitude 16 * @property {number} zoom - Zoom level 17 */ 18 19// Add shared bounds variable at the top level 20let sharedMapBounds = null; 21 22/** 23 * Utility Module - Contains helper functions used across other modules 24 */ 25const OUMUtils = (function () { 26 function getParameterByName(name) { 27 name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]"); 28 const regex = new RegExp("[\\?&]" + name + "=([^&#]*)"), 29 results = regex.exec(location.search); 30 return results === null 31 ? "" 32 : decodeURIComponent(results[1].replace(/\+/g, " ")); 33 } 34 35 function latLngToBounds(lat, lng, zoom, width, height) { 36 // Convert all inputs to numbers first, handling string inputs 37 lat = 38 typeof lat === "string" 39 ? parseFloat(lat.replace(/['"]+/g, "")) 40 : parseFloat(lat); 41 lng = 42 typeof lng === "string" 43 ? parseFloat(lng.replace(/['"]+/g, "")) 44 : parseFloat(lng); 45 zoom = 46 typeof zoom === "string" 47 ? parseFloat(zoom.replace(/['"]+/g, "")) 48 : parseFloat(zoom); 49 width = 50 typeof width === "string" 51 ? parseFloat(width.replace(/['"]+/g, "")) 52 : parseFloat(width); 53 height = 54 typeof height === "string" 55 ? parseFloat(height.replace(/['"]+/g, "")) 56 : parseFloat(height); 57 58 // Validate coordinates 59 if (!validateCoordinates(lat, lng)) { 60 console.warn("Invalid coordinates for latLngToBounds, using defaults"); 61 return [ 62 [OUMConfig.defaults.map.lat, OUMConfig.defaults.map.lng], 63 [OUMConfig.defaults.map.lat, OUMConfig.defaults.map.lng], 64 ]; 65 } 66 67 // Validate dimensions 68 if (isNaN(width) || width <= 0 || isNaN(height) || height <= 0) { 69 console.warn("Invalid dimensions for latLngToBounds"); 70 width = 520; // Default width 71 height = 294; // Default height 72 } 73 74 // Validate and adjust zoom 75 if (isNaN(zoom)) { 76 zoom = OUMConfig.defaults.map.zoom; 77 } else { 78 // Ensure zoom is between 1 and 20 79 zoom = Math.max(1, Math.min(20, zoom)); 80 } 81 82 // Use Leaflet's projection system to properly account for Mercator distortion 83 // This ensures equal visual margins regardless of latitude 84 const crs = L.CRS.EPSG3857; // Default Mercator projection used by Leaflet 85 86 // Project the center position to pixel space at the specified zoom level 87 const centerLatLng = L.latLng(lat, lng); 88 const centerPoint = crs.latLngToPoint(centerLatLng, zoom); 89 90 // Calculate pixel bounds: center ± half width/height 91 const halfWidth = width / 2; 92 const halfHeight = height / 2; 93 94 // Calculate the four corners of the viewport in pixel space 95 const topLeftPoint = L.point(centerPoint.x - halfWidth, centerPoint.y - halfHeight); 96 const topRightPoint = L.point(centerPoint.x + halfWidth, centerPoint.y - halfHeight); 97 const bottomLeftPoint = L.point(centerPoint.x - halfWidth, centerPoint.y + halfHeight); 98 const bottomRightPoint = L.point(centerPoint.x + halfWidth, centerPoint.y + halfHeight); 99 100 // Unproject the corners back to lat/lng (this properly accounts for Mercator distortion) 101 const topLeftLatLng = crs.pointToLatLng(topLeftPoint, zoom); 102 const topRightLatLng = crs.pointToLatLng(topRightPoint, zoom); 103 const bottomLeftLatLng = crs.pointToLatLng(bottomLeftPoint, zoom); 104 const bottomRightLatLng = crs.pointToLatLng(bottomRightPoint, zoom); 105 106 // Find the min/max lat and lng from all four corners 107 // This ensures we capture the full bounds correctly 108 const minLat = Math.min(topLeftLatLng.lat, topRightLatLng.lat, bottomLeftLatLng.lat, bottomRightLatLng.lat); 109 const maxLat = Math.max(topLeftLatLng.lat, topRightLatLng.lat, bottomLeftLatLng.lat, bottomRightLatLng.lat); 110 const minLng = Math.min(topLeftLatLng.lng, topRightLatLng.lng, bottomLeftLatLng.lng, bottomRightLatLng.lng); 111 const maxLng = Math.max(topLeftLatLng.lng, topRightLatLng.lng, bottomLeftLatLng.lng, bottomRightLatLng.lng); 112 113 return [ 114 [minLat, minLng], // Southwest corner 115 [maxLat, maxLng], // Northeast corner 116 ]; 117 } 118 119 function customAutoSuggestText(text, val) { 120 return ( 121 '<div><img src="' + 122 val.layer.options.icon.options.iconUrl + 123 '" />' + 124 val.layer.options.title + 125 "</div>" 126 ); 127 } 128 129 function initGeosearchProvider() { 130 let provider; 131 switch (oum_geosearch_provider) { 132 case "osm":
133 provider = new GeoSearch.OpenStreetMapProvider(); 134 break; 135 case "geoapify": 136 provider = new GeoSearch.GeoapifyProvider({ 137 params: { 138 apiKey: oum_geosearch_provider_geoapify_key, 139 }, 140 }); 141 break; 142 case "here": 143 provider = new GeoSearch.HereProvider({ 144 params: { 145 apiKey: oum_geosearch_provider_here_key, 146 }, 147 }); 148 break; 149 case "mapbox": 150 provider = new GeoSearch.MapBoxProvider({ 151 params: { 152 access_token: oum_geosearch_provider_mapbox_key, 153 }, 154 }); 155 break; 156 default: 157 provider = new GeoSearch.OpenStreetMapProvider(); 158 break; 159 } 160 return provider; 161 } 162 163 /** 164 * Validates and sanitizes coordinates 165 * @param {number|string} lat 166 * @param {number|string} lng 167 * @returns {boolean} 168 */ 169 function validateCoordinates(lat, lng) { 170 const parsedLat = parseFloat(lat); 171 const parsedLng = parseFloat(lng); 172 return ( 173 !isNaN(parsedLat) && 174 !isNaN(parsedLng) && 175 parsedLat >= -90 && 176 parsedLat <= 90 && 177 parsedLng >= -180 && 178 parsedLng <= 180 179 ); 180 } 181 182 /** 183 * Safely parses a JSON string 184 * @param {string} str 185 * @param {*} fallback 186 * @returns {*} 187 */ 188 function safeJSONParse(str, fallback = null) { 189 try { 190 return JSON.parse(str); 191 } catch (e) { 192 return fallback; 193 } 194 } 195 196 /** 197 * Debounces a function 198 * @param {Function} func 199 * @param {number} wait 200 * @returns {Function} 201 */ 202 function debounce(func, wait = 250) { 203 let timeout; 204 return function executedFunction(...args) { 205 const later = () => { 206 clearTimeout(timeout); 207 func(...args); 208 }; 209 clearTimeout(timeout); 210 timeout = setTimeout(later, wait); 211 }; 212 } 213 214 /** 215 * Simple sprintf implementation for string formatting 216 * @param {string} str - String with placeholders (%1$d, %2$s, etc) 217 * @param {...any} args - Values to replace placeholders 218 * @returns {string} 219 */ 220 function sprintf(str, ...args) { 221 return str
222 .replace(/\\n/g, "\n") 223 .replace(/%(\d+)\$[sd]/g, (match, num) => 224 typeof args[num - 1] !== "undefined" ? args[num - 1] : match 225 ); 226 } 227 228 // Public interface 229 return { 230 getParameterByName, 231 latLngToBounds, 232 customAutoSuggestText, 233 initGeosearchProvider, 234 validateCoordinates, 235 safeJSONParse, 236 debounce, 237 sprintf 238 }; 239})(); 240 241/** 242 * Error Handler Module - Centralizes error management 243 */ 244const OUMErrorHandler = (function () { 245 function showError(message, type = "error") { 246 console.error(`OUM Error: ${message}`); 247 248 // Show error in UI if error container exists 249 const errorContainer = document.getElementById("oum_add_location_error"); 250 if (errorContainer) { 251 errorContainer.innerHTML = `${message}<br>`; 252 errorContainer.style.display = "block"; 253 } 254 } 255 256 function handleAjaxError(error) { 257 showError(`Ajax request failed: ${error.message}`); 258 } 259 260 function validateCoordinates(lat, lng) { 261 const parsedLat = parseFloat(lat); 262 const parsedLng = parseFloat(lng); 263 264 if (isNaN(parsedLat) || isNaN(parsedLng)) { 265 showError("Invalid coordinates provided"); 266 return false; 267 } 268 269 if ( 270 parsedLat < -90 || 271 parsedLat > 90 || 272 parsedLng < -180 || 273 parsedLng > 180 274 ) { 275 showError("Coordinates out of valid range"); 276 return false; 277 } 278 279 return true; 280 } 281 282 return { 283 showError, 284 handleAjaxError, 285 validateCoordinates, 286 }; 287})(); 288 289/** 290 * Configuration Module - Centralizes all configuration settings 291 */ 292const OUMConfig = (function () { 293 // Private variables 294 const defaults = { 295 map: { 296 lat: 26, 297 lng: 10, 298 zoom: 1, 299 bounds: L.latLngBounds( 300 L.latLng(-85, -200), // Southwest corner (adjusted to prevent grey areas) 301 L.latLng(85, 200) // Northeast corner (adjusted to prevent grey areas) 302 ), 303 }, 304 media: { 305 maxFiles: 5, 306 validImageExtensions: ["jpeg", "jpg", "png", "webp"], 307 maxImageSize: (window.oum_max_image_filesize || 10) * 1048576, // Convert MB to bytes 308 }, 309 search: { 310 zoomLevel: window.oum_searchmarkers_zoom || 8, 311 addressLabel: window.oum_searchaddress_label || "Search for address", 312 markersLabel: window.oum_searchmarkers_label || "Find marker", 313 }, 314 }; 315 316 function getMapStyle() { 317 return window.mapStyle || "OpenFreeMap.bright"; 318 } 319 320 function getTileProviderKey() { 321 return window.oum_tile_provider_mapbox_key || ""; 322 } 323 324 function getCartoTileProviderKey() { 325 return window.oum_tile_provider_carto_key || ""; 326 } 327 328 function getGeosearchProvider() { 329 let provider; 330 switch (oum_geosearch_provider) { 331 case "geoapify": 332 provider = new GeoSearch.GeoapifyProvider({ 333 params: { 334 apiKey: oum_geosearch_provider_geoapify_key, 335 }, 336 }); 337 break; 338 case "here": 339 provider = new GeoSearch.HereProvider({ 340 params: { 341 apiKey: oum_geosearch_provider_here_key, 342 }, 343 }); 344 break; 345 case "mapbox": 346 provider = new GeoSearch.MapBoxProvider({ 347 params: { 348 access_token: oum_geosearch_provider_mapbox_key, 349 }, 350 }); 351 break; 352 default: 353 provider = new GeoSearch.OpenStreetMapProvider(); 354 } 355 return provider; 356 } 357 358 return { 359 defaults, 360 getMapStyle, 361 getTileProviderKey, 362 getCartoTileProviderKey, 363 getGeosearchProvider, 364 }; 365})(); 366 367/** 368 * Map Core Module - Handles the main map initialization and configuration 369 */ 370const OUMMap = (function () { 371 // Private variables 372 let map = null; 373 let world_bounds = null; 374 let startPosition = { 375 lat: 376 typeof start_lat !== "undefined" 377 ? Number(start_lat) 378 : OUMConfig.defaults.map.lat, 379 lng: 380 typeof start_lng !== "undefined" 381 ? Number(start_lng) 382 : OUMConfig.defaults.map.lng, 383 zoom: 384 typeof start_zoom !== "undefined" 385 ? Number(start_zoom) 386 : OUMConfig.defaults.map.zoom, 387 }; 388 389 // Private functions 390 function initializeStartPosition() { 391 // Validate coordinates 392 if (!OUMUtils.validateCoordinates(startPosition.lat, startPosition.lng)) { 393 console.warn("Invalid coordinates, using defaults"); 394 startPosition.lat = OUMConfig.defaults.map.lat; 395 startPosition.lng = OUMConfig.defaults.map.lng; 396 } 397 398 // Validate zoom level (between 1 and 20) 399 if ( 400 isNaN(startPosition.zoom) || 401 startPosition.zoom < 1 || 402 startPosition.zoom > 20 403 ) { 404 console.warn("Invalid zoom level, using default"); 405 startPosition.zoom = OUMConfig.defaults.map.zoom; 406 } 407 } 408 409 // Leaflet.markercluster expects discrete zoom steps to keep cluster membership stable. 410 function isClusterZoomLocked() { 411 return !!oum_enable_cluster; 412 } 413 414 // Normalize programmatic zoom values so clustered maps never start on fractional zooms. 415 function normalizeMainMapZoom(zoom) { 416 const parsedZoom = Number(zoom); 417 if (isNaN(parsedZoom)) { 418 return zoom; 419 } 420 421 return isClusterZoomLocked() ? Math.round(parsedZoom) : parsedZoom; 422 } 423
424 // Route main-map setView calls through the same zoom normalization logic. 425 function setMainMapView(center, zoom, options = {}) { 426 map.setView(center, normalizeMainMapZoom(zoom), options); 427 } 428 429 // Keep animated moves aligned with the integer zoom used by clustered maps. 430 function flyMainMapTo(center, zoom, options = {}) { 431 map.flyTo(center, normalizeMainMapZoom(zoom), options); 432 } 433 434 // Clamp flyToBounds to an integer max zoom so cluster refreshes stay deterministic. 435 function flyMainMapToBounds(bounds, options = {}) { 436 const flyOptions = { ...options }; 437 438 if (isClusterZoomLocked()) { 439 flyOptions.maxZoom = normalizeMainMapZoom(map.getBoundsZoom(bounds, false)); 440 } 441 442 map.flyToBounds(bounds, flyOptions); 443 } 444 445 function setupMapBounds() { 446 // Set bounds if fixed map bounds is enabled 447 if (oum_enable_fixed_map_bounds) { 448 // Calculate bounds using the updated latLngToBounds method 449 // This uses Leaflet's projection system to properly account for Mercator distortion 450 // Settings map dimensions (used when setting initial view in admin) 451 const boundsArray = OUMUtils.latLngToBounds( 452 startPosition.lat, 453 startPosition.lng, 454 startPosition.zoom, 455 520, // Width of settings map 456 294 // Height of settings map 457 ); 458 459 // Convert the bounds array to a Leaflet LatLngBounds object 460 world_bounds = L.latLngBounds( 461 L.latLng(boundsArray[0][0], boundsArray[0][1]), 462 L.latLng(boundsArray[1][0], boundsArray[1][1]) 463 ); 464 465 // Store bounds globally for form map to use 466 sharedMapBounds = world_bounds; 467 } else { 468 // Use default world bounds when fixed map bounds is disabled 469 world_bounds = OUMConfig.defaults.map.bounds; 470 sharedMapBounds = world_bounds; 471 } 472 473 // Set the minimum zoom level to prevent zooming out too far 474 const maxVisibleBounds = map.getBoundsZoom(world_bounds); 475 map.setMinZoom(maxVisibleBounds); 476 477 // Set max bounds without padding 478 map.setMaxBounds(world_bounds); 479 480 let isAdjusting = false; 481 482 // Handle map movement without recursion 483 map.on("moveend", function () { 484 if (isAdjusting) return; 485 isAdjusting = true; 486 487 const zoom = map.getZoom(); 488 489 // Only enforce bounds if we're zoomed in beyond the minimum zoom 490 if (zoom > maxVisibleBounds) { 491 const currentBounds = map.getBounds(); 492 const currentCenter = map.getCenter(); 493 494 let needsAdjustment = false; 495 let newLat = currentCenter.lat; 496 let newLng = currentCenter.lng; 497 498 // Calculate current viewport dimensions 499 const viewportHeight = 500 currentBounds.getNorth() - currentBounds.getSouth(); 501 const viewportWidth = 502 currentBounds.getEast() - currentBounds.getWest(); 503 504 // Check and adjust latitude (north/south) 505 if (currentBounds.getNorth() > world_bounds.getNorth()) { 506 newLat = world_bounds.getNorth() - viewportHeight / 2; 507 needsAdjustment = true; 508 } else if (currentBounds.getSouth() < world_bounds.getSouth()) { 509 newLat = world_bounds.getSouth() + viewportHeight / 2; 510 needsAdjustment = true; 511 } 512 513 // Check and adjust longitude (east/west) 514 if (currentBounds.getEast() > world_bounds.getEast()) { 515 newLng = world_bounds.getEast() - viewportWidth / 2; 516 needsAdjustment = true; 517 } else if (currentBounds.getWest() < world_bounds.getWest()) { 518 newLng = world_bounds.getWest() + viewportWidth / 2; 519 needsAdjustment = true; 520 } 521 522 if (needsAdjustment) { 523 setMainMapView([newLat, newLng], zoom, { animate: false }); 524 } 525 } 526 527 isAdjusting = false; 528 }); 529 } 530 531 function setupTileLayer(mapStyle) { 532 return window.OUMMapStyles.addBaseLayer(map, mapStyle, { 533 mapboxKey: OUMConfig.getTileProviderKey(), 534 cartoKey: OUMConfig.getCartoTileProviderKey(), 535 setupCustomImageLayer: setupCustomImageLayer, 536 customImageHideTiles: window.oum_custom_image_hide_tiles,
537 customImageBackgroundColor: window.oum_custom_image_background_color, 538 }); 539 } 540 541 function setupCustomImageLayer() { 542 // Check if we have an image URL and bounds 543 if (typeof window.oum_custom_image_url !== 'undefined' && window.oum_custom_image_url && 544 typeof window.oum_custom_image_bounds !== 'undefined' && window.oum_custom_image_bounds) { 545 546 // Check if the uploaded file is an SVG 547 const isSVG = window.oum_custom_image_url.toLowerCase().includes('.svg'); 548 549 if (isSVG) { 550 // Handle SVG file - fetch and render as DOM elements 551 setupSVGFromFile(); 552 } else { 553 // Handle regular image file 554 setupImageOverlay(); 555 } 556 } else { 557 } 558 } 559 560 // Helper function to setup SVG from uploaded file 561 function setupSVGFromFile() { 562 try { 563 // Get bounds data (now properly handled as object) 564 const bounds = window.oum_custom_image_bounds; 565 566 // Validate bounds 567 if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' || 568 typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' || 569 bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') { 570 console.warn('Open User Map: Invalid or empty bounds data, skipping SVG file layer'); 571 return; 572 } 573 574 575 // Fetch the SVG file and render it 576 fetch(window.oum_custom_image_url) 577 .then(response => response.text()) 578 .then(svgText => { 579 // Create SVG element from the fetched content 580 const svgElement = createSVGElement(svgText); 581 if (!svgElement) { 582 console.warn('Open User Map: Cannot create SVG layer from file - invalid SVG element'); 583 return; 584 } 585 586 // Create a custom SVG layer 587 const svgLayer = L.svgOverlay(svgElement, [ 588 [bounds.south, bounds.west], // Southwest corner 589 [bounds.north, bounds.east] // Northeast corner 590 ], { 591 opacity: 1.0, 592 interactive: true 593 }); 594 595 svgLayer.addTo(map); 596 597 598 // Store reference for potential removal 599 window.oumCustomSVGLayer = svgLayer; 600 601 console.log('Open User Map: Custom SVG file layer added successfully'); 602 }) 603 .catch(error => { 604 console.warn('Open User Map: Error fetching SVG file:', error); 605 }); 606 607 } catch (error) { 608 console.warn('Open User Map: Error setting up custom SVG file layer:', error); 609 } 610 } 611 612 // Helper function to setup regular image overlay 613 function setupImageOverlay() { 614 try { 615 // Get bounds data (now properly handled as object) 616 const bounds = window.oum_custom_image_bounds; 617 618 // Validate bounds 619 if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' || 620 typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' || 621 bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') { 622 console.warn('Open User Map: Invalid or empty bounds data, skipping image layer'); 623 return; 624 } 625 626 627 // Create image overlay 628 const imageOverlay = L.imageOverlay(window.oum_custom_image_url, [ 629 [bounds.south, bounds.west], // Southwest corner 630 [bounds.north, bounds.east] // Northeast corner 631 ], { 632 opacity: 1.0, 633 interactive: true 634 }); 635 636 imageOverlay.addTo(map); 637 638 639 // Store reference for potential removal 640 window.oumCustomImageLayer = imageOverlay; 641 642 console.log('Open User Map: Custom image layer added successfully'); 643 644 } catch (error) { 645 console.warn('Open User Map: Error setting up custom image layer:', error); 646 } 647 } 648 649 // Helper function to create SVG element from text 650 function createSVGElement(svgText) { 651 // Create a temporary div to parse the SVG 652 const tempDiv = document.createElement('div'); 653 tempDiv.innerHTML = svgText; 654 const svgElement = tempDiv.querySelector('svg'); 655 656 if (!svgElement) { 657 console.warn('Open User Map: No valid SVG element found in SVG text'); 658 return null; 659 } 660 661 // Preserve the original viewBox if it exists 662 // If missing, try to create it from width/height attributes 663 if (!svgElement.getAttribute('viewBox')) { 664 const width = svgElement.getAttribute('width'); 665 const height = svgElement.getAttribute('height'); 666 if (width && height) { 667 // Remove units if present (e.g., "1580px" -> "1580") 668 const widthNum = parseFloat(width); 669 const heightNum = parseFloat(height); 670 if (!isNaN(widthNum) && !isNaN(heightNum)) { 671 svgElement.setAttribute('viewBox', `0 0 ${widthNum} ${heightNum}`); 672 } else { 673 svgElement.setAttribute('viewBox', '0 0 1000 1200'); 674 } 675 } else { 676 svgElement.setAttribute('viewBox', '0 0 1000 1200'); 677 } 678 } 679 680 // Ensure the SVG has proper styling for overlay 681 svgElement.style.width = '100%'; 682 svgElement.style.height = '100%'; 683 svgElement.style.display = 'block'; 684 685 // Ensure the SVG fills the entire bounds area to prevent cropping 686 svgElement.setAttribute('preserveAspectRatio', 'none'); 687 688 console.log('Open User Map: SVG element created successfully:', svgElement); 689 690 return svgElement; 691 } 692 693 function setupControls() { 694 // Search markers control as button 695 if (oum_enable_searchmarkers_button) { 696 L.control 697 .search({ 698 textPlaceholder: oum_searchmarkers_label, 699 layer: window.oumMarkersLayer, 700 propertyName: "content", 701 initial: false, 702 buildTip: OUMUtils.customAutoSuggestText, 703 autoType: false, 704 firstTipSubmit: true, 705 autoCollapse: true, 706 zoom: oum_searchmarkers_zoom, 707 }) 708 .addTo(map); 709 } 710 711 // Search markers control as searchbar 712 if (oum_enable_searchbar && oum_searchbar_type === "markers") { 713 const markerSearchBar = L.control.search({ 714 textPlaceholder: oum_searchmarkers_label, 715 layer: window.oumMarkersLayer, 716 propertyName: "content", 717 initial: false, 718 buildTip: OUMUtils.customAutoSuggestText, 719 autoType: false, 720 firstTipSubmit: true, 721 autoCollapse: false,
722 collapsed: false, 723 zoom: oum_searchmarkers_zoom, 724 container: 'oum_search_marker' 725 }); 726 markerSearchBar.addTo(map); 727 // Do not leave the internal search "content" string in the input after a result is chosen 728 markerSearchBar.on("search:locationfound", function () { 729 this._input.value = ""; 730 if (this._cancel) { 731 this._cancel.style.display = "none"; 732 } 733 }); 734 } 735 736 // Add searchbar for address search 737 if (oum_enable_searchbar && oum_searchbar_type === "address") { 738 const searchBar = new GeoSearch.GeoSearchControl({ 739 style: "bar", 740 showMarker: false, 741 provider: OUMUtils.initGeosearchProvider(), 742 searchLabel: oum_searchaddress_label, 743 updateMap: false, 744 }); 745 map.addControl(searchBar); 746 relocateAddressSearchBar(); 747 } 748 749 // Add button for address search 750 if (oum_enable_searchaddress_button) { 751 const searchButton = new GeoSearch.GeoSearchControl({ 752 style: "button", 753 showMarker: false, 754 provider: OUMUtils.initGeosearchProvider(), 755 searchLabel: oum_searchaddress_label, 756 updateMap: false, 757 }); 758 map.addControl(searchButton); 759 } 760 761 // Current location control 762 if (oum_enable_currentlocation) { 763 window.map_locate_process = L.control 764 .locate({ 765 flyTo: true, 766 showPopup: false, 767 }) 768 .addTo(map); 769 } 770 } 771 772 // Move the Address searchbar into Advanced Filter when that section is present. 773 function relocateAddressSearchBar() { 774 const target = document.getElementById("oum_search_address"); 775 if (!target || !map) { 776 return; 777 } 778 779 const mapContainer = map.getContainer(); 780 const bars = mapContainer.querySelectorAll(".leaflet-control-geosearch.leaflet-geosearch-bar"); 781 let bar = null; 782 bars.forEach(function (el) { 783 if (!el.closest(".leaflet-top") && !el.closest(".leaflet-bottom")) { 784 bar = el; 785 } 786 }); 787 if (!bar && bars.length) { 788 bar = bars[0]; 789 } 790 if (bar) { 791 target.appendChild(bar); 792 } 793 } 794 795 function setupMapEvents() { 796 // Event: pan or zoom Map 797 map.on("moveend", function (ev) { 798 startPosition.lat = map.getCenter().lat; 799 startPosition.lng = map.getCenter().lng; 800 startPosition.zoom = map.getZoom(); 801 }); 802 803 // Event: Enter/Exit Fullscreen 804 const addLocationPopup = document.querySelector("#add-location-overlay"); 805 const originalContainer = addLocationPopup?.parentElement; 806 const fullscreenContainer = document.querySelector( 807 ".open-user-map .map-wrap" 808 ); 809 810 if (addLocationPopup) { 811 map.on("enterFullscreen", function () { 812 fullscreenContainer.appendChild(addLocationPopup); 813 }); 814 815 map.on("exitFullscreen", function () { 816 originalContainer.appendChild(addLocationPopup); 817 }); 818 } 819 820 // Event: geosearch success 821 map.on("geosearch/showlocation", function(e) { 822 let coords = e.marker._latlng; 823 824 // Check against fixed map bounds if enabled, otherwise allow any location 825 let isInBounds = true; // Default to true when fixed bounds is disabled 826 if (oum_enable_fixed_map_bounds === 'on' && sharedMapBounds) { 827 // Check if location is within the fixed map bounds (not just current viewport) 828 isInBounds = sharedMapBounds.contains(coords); 829 } 830 831 if (!isInBounds && oum_enable_fixed_map_bounds === 'on') { 832 console.log("This search result is out of reach."); 833 const searchBar = document.querySelector(`#${map_el} .leaflet-geosearch-bar form`) 834 || document.querySelector("#oum_search_address form") 835 || document.querySelector(".oum-searchbar-filter .leaflet-geosearch-bar form"); 836 if (searchBar) { 837 searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)"; 838 setTimeout(function () { 839 searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)"; 840 }, 2000); 841 } 842 } else { 843 // Handle valid search result 844 if (e.location.bounds) { 845 flyMainMapToBounds(e.location.bounds); 846 } else if (e.location.raw && e.location.raw.mapView) { 847 flyMainMapToBounds([ 848 [e.location.raw.mapView.south, e.location.raw.mapView.west], 849 [e.location.raw.mapView.north, e.location.raw.mapView.east] 850 ]); 851 } else { 852 flyMainMapTo([e.location.y, e.location.x], 17); 853 } 854 } 855 }); 856 } 857 858 function setupRegionEvents() { 859 document 860 .querySelectorAll(".open-user-map .change_region") 861 .forEach(function (btn) { 862 btn.onclick = function (event) { 863 let el = event.currentTarget; 864 let region_lat = el.getAttribute("data-lat"); 865 let region_lng = el.getAttribute("data-lng"); 866 let region_zoom = el.getAttribute("data-zoom"); 867 868 // Get frontend map container size 869 const mapContainer = map.getContainer(); 870 const frontendMapWidth = mapContainer.offsetWidth || 520; 871 const frontendMapHeight = mapContainer.offsetHeight || 294; 872 const settingsMapWidth = 520; 873 const settingsMapHeight = 294; 874 875 // Calculate bounds based on settings map dimensions 876 let region_bounds = OUMUtils.latLngToBounds( 877 parseFloat(region_lat), 878 parseFloat(region_lng), 879 parseFloat(region_zoom), 880 settingsMapWidth, 881 settingsMapHeight 882 ); 883 884 // Convert to Leaflet bounds 885 const regionBounds = L.latLngBounds( 886 L.latLng(region_bounds[0][0], region_bounds[0][1]), 887 L.latLng(region_bounds[1][0], region_bounds[1][1]) 888 ); 889 890 // Calculate aspect ratios 891 const settingsAspectRatio = settingsMapWidth / settingsMapHeight; 892 const frontendAspectRatio = frontendMapWidth / frontendMapHeight; 893 const aspectRatioDifference = Math.abs(settingsAspectRatio - frontendAspectRatio); 894 895 let region_bounds_zoom; 896 897 // If aspect ratios match, use ratio-based calculation 898 if (aspectRatioDifference < 0.01) { 899 const widthRatio = frontendMapWidth / settingsMapWidth; 900 const zoomAdjustment = Math.log2(widthRatio); 901 region_bounds_zoom = parseFloat(region_zoom) + zoomAdjustment; 902 } else { 903 // Different aspect ratios - use getBoundsZoom 904 region_bounds_zoom = map.getBoundsZoom(regionBounds, false); 905 } 906 907 // Center Map 908 flyMainMapTo([region_lat, region_lng], region_bounds_zoom); 909 910 // Update active state 911 document 912 .querySelectorAll(".open-user-map .change_region") 913 .forEach(function (el) { 914 el.classList.remove("active"); 915 }); 916 el.classList.add("active"); 917 }; 918 919 // Event: Change Region on ?region=Europe 920 const REGION_ID = OUMUtils.getParameterByName("region"); 921 if (btn.textContent == REGION_ID) { 922 btn.click(); 923 } 924 }); 925 } 926 927 // Public interface 928 return { 929 init: function (mapEl) {
930 try { 931 initializeStartPosition(); 932 933 // Initialize map 934 map = L.map(mapEl, { 935 gestureHandling: oum_enable_scrollwheel_zoom_map ? false : true, 936 minZoom: 1, // Set default minimum zoom 937 zoomSnap: isClusterZoomLocked() ? 1 : 0.01, // MarkerCluster needs integer zoom steps. 938 zoomDelta: 1, // Zoom step size for controls 939 attributionControl: true, 940 fullscreenControl: oum_enable_fullscreen, 941 fullscreenControlOptions: { 942 position: "topleft", 943 fullscreenElement: document.querySelector( 944 ".open-user-map .map-wrap" 945 ), 946 }, 947 }); 948 949 map.attributionControl.setPrefix(false); 950 951 // First set up the tile layer 952 setupTileLayer(OUMConfig.getMapStyle()); 953 954 // Invalidate size to ensure accurate measurements 955 map.invalidateSize(); 956 957 // Get the actual frontend map container size 958 const mapContainer = map.getContainer(); 959 const frontendMapWidth = mapContainer.offsetWidth || 520; 960 const frontendMapHeight = mapContainer.offsetHeight || 294; 961 962 // Settings map dimensions (fixed) 963 const settingsMapWidth = 520; 964 const settingsMapHeight = 294; 965 966 // Calculate aspect ratios to determine if they match 967 const settingsAspectRatio = settingsMapWidth / settingsMapHeight; 968 const frontendAspectRatio = frontendMapWidth / frontendMapHeight; 969 const aspectRatioDifference = Math.abs(settingsAspectRatio - frontendAspectRatio); 970 971 // Calculate initial bounds based on settings map dimensions (520) 972 // This gives us the geographic area that should be visible 973 const settingsBoundsArray = OUMUtils.latLngToBounds( 974 startPosition.lat, 975 startPosition.lng, 976 startPosition.zoom, 977 settingsMapWidth, 978 settingsMapHeight 979 ); 980 981 // Convert to Leaflet bounds 982 const settingsBounds = L.latLngBounds( 983 L.latLng(settingsBoundsArray[0][0], settingsBoundsArray[0][1]), 984 L.latLng(settingsBoundsArray[1][0], settingsBoundsArray[1][1]) 985 ); 986 987 let targetZoom; 988 989 // If aspect ratios are very similar (within 1%), use ratio-based zoom calculation 990 // This preserves the exact same geographic area when aspect ratios match 991 if (aspectRatioDifference < 0.01) { 992 // Calculate zoom adjustment based on size ratio 993 // If frontend map is larger, we need higher zoom to show same area 994 // Formula: zoom adjustment = log2(frontendWidth / settingsWidth) 995 const widthRatio = frontendMapWidth / settingsMapWidth; 996 const zoomAdjustment = Math.log2(widthRatio); 997 targetZoom = startPosition.zoom + zoomAdjustment; 998 } else { 999 // Aspect ratios differ - use getBoundsZoom to account for different aspect ratios 1000 // This will fit the bounds but may show slightly different area due to aspect ratio 1001 targetZoom = map.getBoundsZoom(settingsBounds, false); 1002 } 1003 targetZoom = normalizeMainMapZoom(targetZoom); 1004 1005 // Use setView with the calculated zoom level to show the same geographic area 1006 // This ensures the same geographic bounds are visible regardless of frontend map size 1007 setMainMapView([startPosition.lat, startPosition.lng], targetZoom, { 1008 animate: false 1009 }); 1010 1011 // Set up other components 1012 setupMapBounds(); 1013 setupMapEvents(); 1014 setupRegionEvents(); 1015 1016 window.oumMap = map; 1017 1018 // Listen for markers initialized event 1019 document.addEventListener('oum:markers_initialized', function(e) { 1020 if (e.detail.mapId === mapEl) { 1021 // Set up controls after markers are ready 1022 setupControls(); 1023 } 1024 }, { once: true }); // Use once: true to ensure it only runs once 1025 1026 // Dispatch map initialized event when everything is ready 1027 document.dispatchEvent(new CustomEvent('oum:map_initialized', { 1028 detail: { 1029 mapId: mapEl, 1030 map: oumMap 1031 } 1032 })); 1033 1034 return map; 1035 } catch (error) { 1036 OUMErrorHandler.showError("Error initializing map: " + error.message); 1037 throw error; 1038 } 1039 }, 1040 getMap: function () { 1041 return map; 1042 }, 1043 getStartPosition: function () { 1044 return startPosition; 1045 }, 1046 }; 1047})(); 1048 1049/** 1050 * Markers Module - Handles all marker-related functionality 1051 */ 1052const OUMMarkers = (function () { 1053 // Private variables 1054 let markersLayer = null; 1055 let vectorLayer = null; 1056 let allMarkers = []; 1057 let map = null; 1058 let locationsById = {}; 1059 let searchtextFilterValue = ""; 1060 let categoriesFilterSelection = null; 1061 let customfieldsFilterSelection = null; 1062 let visibleMarkersCount = 0;
1063 const categoryFilterSelector = '.open-user-map .oum-marker-categories-filter [name="type"]'; 1064 1065 // Private functions 1066 function initializeMarkersLayer() { 1067 markersLayer = !oum_enable_cluster 1068 ? L.layerGroup({ chunkedLoading: true }) 1069 : L.markerClusterGroup({ 1070 showCoverageOnHover: false, 1071 removeOutsideVisibleBounds: true, 1072 maxClusterRadius: 50, 1073 chunkedLoading: true, 1074 }); 1075 vectorLayer = L.layerGroup(); 1076 } 1077 1078 function oumBubbleLoadingHtml() { 1079 const label = 1080 typeof oum_custom_strings !== "undefined" && oum_custom_strings.bubble_loading 1081 ? String(oum_custom_strings.bubble_loading) 1082 .replace(/\\/g, "\\\\") 1083 .replace(/"/g, """) 1084 : "Loading"; 1085 return ( 1086 '<div class="oum-bubble-loading" role="status" aria-live="polite" aria-busy="true" aria-label="' + 1087 label + 1088 '"><span class="oum-bubble-loading-spinner" aria-hidden="true"></span></div>' 1089 ); 1090 } 1091 1092 function oumBubbleErrorHtml() { 1093 const msg = 1094 typeof oum_custom_strings !== "undefined" && oum_custom_strings.bubble_load_error 1095 ? oum_custom_strings.bubble_load_error 1096 : "Unable to load location details."; 1097 return '<div class="oum-bubble-error" role="alert">' + msg + "</div>"; 1098 } 1099 1100 /** 1101 * After popup HTML is set: fullscreen mirror, edit button, votes/stars, opening hours. 1102 */ 1103 function finalizeMarkerBubbleUI(popup) { 1104 const sourceLayer = popup && popup._source ? popup._source : null; 1105 const el = document.querySelector(".open-user-map #location-fullscreen-container"); 1106 const locationContentWrap = el ? el.querySelector(".location-content-wrap") : null; 1107 if (locationContentWrap) { 1108 locationContentWrap.innerHTML = popup.getContent(); 1109 injectVectorMeasurement(locationContentWrap, sourceLayer); 1110 } 1111 if (el) { 1112 el.classList.add("visible"); 1113 document.querySelector("body").classList.add("oum-location-opened"); 1114 } 1115 1116 if (locationContentWrap) { 1117 checkEditPermissionAndInjectButton(locationContentWrap); 1118 } 1119 1120 const popupContent = popup.getElement(); 1121 if (popupContent) { 1122 injectVectorMeasurement(popupContent, sourceLayer); 1123 checkEditPermissionAndInjectButton(popupContent); 1124 } 1125 1126 if (window.OUMVoteHandler && window.OUMVoteHandler.updateVoteButtonStates) { 1127 window.OUMVoteHandler.updateVoteButtonStates(); 1128 } 1129 1130 if (window.OUMVoteHandler && window.OUMVoteHandler.initializeVoteCountsFromData) { 1131 const voteButtons = el ? el.querySelectorAll(".oum_vote_button") : []; 1132 if (voteButtons.length > 0) { 1133 window.OUMVoteHandler.initializeVoteCountsFromData(); 1134 } 1135 } 1136 1137 if (window.OUMVoteHandler && window.OUMVoteHandler.initializeStarRatings) { 1138 const starRatings = el ? el.querySelectorAll(".oum_star_rating") : []; 1139 if (starRatings.length > 0) { 1140 window.OUMVoteHandler.initializeStarRatings(); 1141 } 1142 } 1143 1144 if (locationContentWrap) { 1145 OUMOpeningHours.setupToggle(locationContentWrap); 1146 } 1147 if (popupContent) { 1148 OUMOpeningHours.setupToggle(popupContent); 1149 } 1150 } 1151 1152 function injectVectorMeasurement(container, layer) { 1153 if (!container || !layer || !layer.options || !layer.options.oumMeasurementLabel) { 1154 return; 1155 } 1156 1157 const title = container.querySelector(".oum_location_title"); 1158 if (!title || title.querySelector(".oum_location_measurement")) { 1159 return; 1160 } 1161 1162 const measurement = document.createElement("span"); 1163 measurement.className = "oum_location_measurement"; 1164 measurement.textContent = layer.options.oumMeasurementLabel; 1165 title.appendChild(measurement); 1166 } 1167 1168 function getPolylineDistanceMeters(latLngs) { 1169 return latLngs.reduce((distance, latLng, index) => { 1170 if (index === 0) { 1171 return distance; 1172 } 1173 1174 return distance + latLngs[index - 1].distanceTo(latLng); 1175 }, 0); 1176 } 1177 1178 function getPolygonAreaSquareMeters(latLngs) { 1179 if (L.GeometryUtil && L.GeometryUtil.geodesicArea) { 1180 return Math.abs(L.GeometryUtil.geodesicArea(latLngs)); 1181 } 1182 1183 return 0; 1184 } 1185 1186 function formatMeters(value) { 1187 if (!isFinite(value) || value <= 0) { 1188 return ""; 1189 } 1190 1191 return value >= 1000 1192 ? `${(value / 1000).toFixed(value >= 10000 ? 0 : 1)} km` 1193 : `${Math.round(value)} m`; 1194 } 1195 1196 function formatSquareMeters(value) { 1197 if (!isFinite(value) || value <= 0) { 1198 return ""; 1199 } 1200
1201 return `${Math.round(value).toLocaleString()} m²`; 1202 } 1203 1204 function createMarker(location) { 1205 // location.content is plain search text from PHP; strip tags keeps backward compatibility if HTML slips in. 1206 const searchBody = (location.content || "") 1207 .toString() 1208 .replace(/(<([^>]+)>)/gi, " ") 1209 .replace(/\s\s+/g, " "); 1210 const contentText = (location.title + " | " + searchBody).toLowerCase(); 1211 1212 let marker = L.marker([location.lat, location.lng], { 1213 title: location.title, 1214 post_id: location.post_id, 1215 content: contentText, 1216 zoom: location.zoom, 1217 icon: L.icon({ 1218 iconUrl: location.icon, 1219 iconSize: [26, 41], 1220 iconAnchor: [13, 41], 1221 popupAnchor: [0, -25], 1222 shadowUrl: marker_shadow_url, 1223 shadowSize: [41, 41], 1224 shadowAnchor: [13, 41], 1225 }), 1226 types: location.types || [], 1227 }); 1228 1229 // Lazy-load full bubble HTML on popup open; placeholder until AJAX returns. 1230 if (typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup) { 1231 const popup = L.responsivePopup().setContent(oumBubbleLoadingHtml()); 1232 marker.bindPopup(popup); 1233 } 1234 1235 return marker; 1236 } 1237 1238 function createPolylineLocation(location) { 1239 const searchBody = (location.content || "") 1240 .toString() 1241 .replace(/(<([^>]+)>)/gi, " ") 1242 .replace(/\s\s+/g, " "); 1243 const contentText = (location.title + " | " + searchBody).toLowerCase(); 1244 const geometry = location.geometry || {}; 1245 const coordinates = geometry.type === "Polygon" && Array.isArray(geometry.coordinates) && Array.isArray(geometry.coordinates[0]) 1246 ? geometry.coordinates[0] 1247 : (Array.isArray(geometry.coordinates) ? geometry.coordinates : []); 1248 const latLngs = coordinates 1249 .filter((coordinate) => Array.isArray(coordinate) && coordinate.length >= 2) 1250 .map((coordinate) => L.latLng(Number(coordinate[1]), Number(coordinate[0]))) 1251 .filter((latLng) => isFinite(latLng.lat) && isFinite(latLng.lng)); 1252 1253 if (latLngs.length < 2) { 1254 return createMarker(location); 1255 } 1256 1257 const first = latLngs[0]; 1258 const last = latLngs[latLngs.length - 1]; 1259 const isPolygon = geometry.type === "Polygon"; 1260 const isClosed = isPolygon || (latLngs.length >= 4 && first.equals(last)); 1261 const color = location.category_color || "#e82c71"; 1262 const measurementLabel = location.show_measurement 1263 ? (isClosed ? formatSquareMeters(getPolygonAreaSquareMeters(latLngs)) : formatMeters(getPolylineDistanceMeters(latLngs))) 1264 : ""; 1265 const layerOptions = { 1266 title: location.title, 1267 post_id: location.post_id, 1268 content: contentText, 1269 zoom: location.zoom, 1270 types: location.types || [], 1271 oumMeasurementLabel: measurementLabel, 1272 color: color, 1273 weight: 4, 1274 opacity: 0.9, 1275 }; 1276 1277 const layer = isClosed 1278 ? L.polygon(latLngs, { 1279 ...layerOptions, 1280 fillColor: color, 1281 fillOpacity: 0.25, 1282 }) 1283 : L.polyline(latLngs, layerOptions); 1284 1285 if (typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup) { 1286 const popup = L.responsivePopup().setContent(oumBubbleLoadingHtml()); 1287 layer.bindPopup(popup); 1288 } 1289 1290 return layer; 1291 } 1292 1293 function createLocationLayer(location) { 1294 return (location.geometry_type === "polyline" || location.geometry_type === "polygon") && location.geometry 1295 ? createPolylineLocation(location) 1296 : createMarker(location); 1297 } 1298 1299 function setupMarkerEvents() { 1300 map.on("popupopen", function (locationBubble) { 1301 const popup = locationBubble.popup; 1302 const marker = popup._source; 1303 1304 const applyBubbleHtml = (html) => { 1305 popup.setContent(html); 1306 if (typeof popup.update === "function") { 1307 popup.update(); 1308 } 1309 finalizeMarkerBubbleUI(popup); 1310 }; 1311 1312 if (!marker || !marker.options || !marker.options.post_id) { 1313 finalizeMarkerBubbleUI(popup); 1314 return; 1315 } 1316 1317 if (marker._oumBubbleHtmlCache) { 1318 applyBubbleHtml(marker._oumBubbleHtmlCache); 1319 return; 1320 } 1321 1322 marker._oumBubbleFetchId = (marker._oumBubbleFetchId || 0) + 1; 1323 const myFetchId = marker._oumBubbleFetchId; 1324 const postId = marker.options.post_id; 1325 1326 popup.setContent(oumBubbleLoadingHtml()); 1327 if (typeof popup.update === "function") { 1328 popup.update(); 1329 } 1330 finalizeMarkerBubbleUI(popup); 1331 1332 if ( 1333 typeof jQuery === "undefined" || 1334 !oum_ajax || 1335 !oum_ajax.ajaxurl 1336 ) { 1337 applyBubbleHtml(oumBubbleErrorHtml()); 1338 return; 1339 } 1340 1341 jQuery.ajax({ 1342 type: "POST", 1343 url: oum_ajax.ajaxurl, 1344 dataType: "json", 1345 data: { 1346 action: oum_ajax.bubble_action || "oum_get_location_bubble", 1347 post_id: postId, 1348 }, 1349 success: function (response) { 1350 if (myFetchId !== marker._oumBubbleFetchId) { 1351 return; 1352 } 1353 if (response && response.success && response.data && response.data.html) { 1354 marker._oumBubbleHtmlCache = response.data.html; 1355 applyBubbleHtml(response.data.html); 1356 } else { 1357 applyBubbleHtml(oumBubbleErrorHtml()); 1358 } 1359 }, 1360 error: function () { 1361 if (myFetchId !== marker._oumBubbleFetchId) { 1362 return; 1363 } 1364 applyBubbleHtml(oumBubbleErrorHtml()); 1365 }, 1366 }); 1367 }); 1368 1369 // Event: Close Location Bubble 1370 map.on("popupclose", function () { 1371 const el = document.querySelector(
1372 ".open-user-map #location-fullscreen-container" 1373 ); 1374 if (el) { 1375 // Clear the content to stop any media playback (YouTube, Vimeo, etc.) 1376 const locationContentWrap = el.querySelector(".location-content-wrap"); 1377 if (locationContentWrap) { 1378 locationContentWrap.innerHTML = ''; 1379 } 1380 el.classList.remove("visible"); 1381 } 1382 document.querySelector("body").classList.remove("oum-location-opened"); 1383 }); 1384 } 1385 1386 /** 1387 * Check edit permission for a location and inject edit button if allowed 1388 * This is called when a popup opens to dynamically show edit buttons 1389 * based on real-time permissions (cache-safe solution) 1390 * 1391 * @param {HTMLElement} container - The container element to search for placeholder 1392 */ 1393 function checkEditPermissionAndInjectButton(container) { 1394 // Find the edit button placeholder in the opened popup 1395 const placeholder = container.querySelector('.edit-location-button-placeholder'); 1396 1397 if (!placeholder) { 1398 return; // No placeholder found, nothing to do 1399 } 1400 1401 const postId = placeholder.getAttribute('data-post-id'); 1402 1403 if (!postId) { 1404 placeholder.remove(); 1405 return; 1406 } 1407 1408 if (typeof jQuery === 'undefined') { 1409 placeholder.remove(); 1410 return; 1411 } 1412 1413 if (!oum_ajax || !oum_ajax.ajaxurl) { 1414 placeholder.remove(); 1415 return; 1416 } 1417 1418 // Make AJAX call to check permissions 1419 jQuery.ajax({ 1420 type: 'POST', 1421 url: oum_ajax.ajaxurl, 1422 dataType: 'json', 1423 data: { 1424 action: 'oum_check_edit_permission', 1425 post_id: postId 1426 }, 1427 success: function(response) { 1428 if (response && response.success && response.data && response.data.can_edit) { 1429 // User can edit - inject the edit button 1430 const editButton = document.createElement('div'); 1431 editButton.className = 'edit-location-button'; 1432 editButton.setAttribute('data-post-id', postId); 1433 editButton.setAttribute('title', oum_custom_strings && oum_custom_strings.edit_location ? oum_custom_strings.edit_location : 'Edit location'); 1434 1435 // Replace placeholder with actual button 1436 placeholder.parentNode.replaceChild(editButton, placeholder); 1437 } else { 1438 // User cannot edit - remove placeholder (keep clean HTML) 1439 placeholder.remove(); 1440 } 1441 }, 1442 error: function(xhr, status, error) { 1443 // On error, just remove the placeholder 1444 placeholder.remove(); 1445 } 1446 }); 1447 } 1448 1449 function applyFilters() { 1450 if (!markersLayer) { 1451 return; 1452 } 1453 1454 markersLayer.clearLayers(); 1455 if (vectorLayer) { 1456 vectorLayer.clearLayers(); 1457 } 1458 const filteredMarkers = []; 1459 1460 allMarkers.forEach((marker) => { 1461 if ( 1462 markerMatchesSearchtextFilter(marker) && 1463 markerMatchesCategoriesFilter(marker) && 1464 markerMatchesCustomfieldsFilter(marker) 1465 ) { 1466 filteredMarkers.push(marker); 1467 } 1468 }); 1469 1470 visibleMarkersCount = filteredMarkers.length; 1471 addMarkersToLayer(filteredMarkers); 1472 1473 if (oum_enable_cluster && typeof markersLayer.refreshClusters === "function") { 1474 markersLayer.refreshClusters(); 1475 } 1476 } 1477 1478 function addMarkersToLayer(markers) { 1479 if (!markersLayer || !markers.length) { 1480 return; 1481 } 1482 1483 const pointMarkers = []; 1484 const vectorMarkers = []; 1485 1486 markers.forEach((marker) => { 1487 if (marker instanceof L.Marker) { 1488 pointMarkers.push(marker); 1489 } else { 1490 vectorMarkers.push(marker); 1491 } 1492 }); 1493 1494 vectorMarkers.forEach((marker) => { 1495 if (vectorLayer) { 1496 vectorLayer.addLayer(marker); 1497 } 1498 }); 1499 1500 if (!pointMarkers.length) { 1501 return; 1502 } 1503 1504 // Batch addLayers() sometimes draws no marker when only one exists; one addLayer() avoids that bug. 1505 if ( 1506 oum_enable_cluster && 1507 pointMarkers.length === 1 && 1508 typeof markersLayer.addLayer === "function" 1509 ) { 1510 markersLayer.addLayer(pointMarkers[0]); 1511 return; 1512 } 1513 1514 // Batch marker insertion when supported to improve large dataset performance. 1515 if (typeof markersLayer.addLayers === "function") { 1516 markersLayer.addLayers(pointMarkers); 1517 return; 1518 } 1519
1520 pointMarkers.forEach((marker) => { 1521 markersLayer.addLayer(marker); 1522 }); 1523 } 1524 1525 function markerMatchesSearchtextFilter(marker) { 1526 if (!searchtextFilterValue) { 1527 return true; 1528 } 1529 return marker.options.content.toLowerCase().includes(searchtextFilterValue); 1530 } 1531 1532 function markerMatchesCategoriesFilter(marker) { 1533 const markerTypes = marker.options.types || []; 1534 1535 if (!markerTypes.length) { 1536 return true; 1537 } 1538 1539 // Before the user interacts with the category checkboxes, keep the previous behaviour (show all) 1540 if (categoriesFilterSelection === null) { 1541 return true; 1542 } 1543 1544 if (!categoriesFilterSelection.size) { 1545 return false; 1546 } 1547 1548 return markerTypes.some((type) => categoriesFilterSelection.has(type)); 1549 } 1550 1551 function markerMatchesCustomfieldsFilter(marker) { 1552 if (!customfieldsFilterSelection || !Object.keys(customfieldsFilterSelection).length) { 1553 return true; 1554 } 1555 1556 const locationData = getLocationData(marker.options.post_id); 1557 if (!locationData) { 1558 return true; 1559 } 1560 1561 for (const [customFieldId, criterion] of Object.entries(customfieldsFilterSelection)) { 1562 // Special handling for opening_hours_open_now filter type 1563 if (criterion.type === 'opening_hours_open_now') { 1564 const field = locationData.custom_fields?.find( 1565 (customField) => String(customField.index) === String(customFieldId) 1566 ); 1567 1568 // Use pre-calculated open_now value from PHP 1569 if (field && typeof field.open_now === 'boolean') { 1570 if (field.open_now !== true) { 1571 return false; 1572 } 1573 } else { 1574 // Fallback: if open_now not available, exclude location 1575 return false; 1576 } 1577 continue; 1578 } 1579 1580 // Standard custom field filtering 1581 const fieldValue = getCustomFieldValue(locationData, customFieldId); 1582 if (!matchesCriterion(fieldValue, criterion)) { 1583 return false; 1584 } 1585 } 1586 1587 return true; 1588 } 1589 1590 function getLocationData(postId) { 1591 if (!postId) { 1592 return null; 1593 } 1594 1595 if (locationsById[String(postId)]) { 1596 return locationsById[String(postId)]; 1597 } 1598 1599 if (typeof window !== "undefined" && Array.isArray(window.oum_all_locations)) { 1600 const fallback = window.oum_all_locations.find( 1601 (location) => String(location.post_id) === String(postId) 1602 ); 1603 if (fallback) { 1604 locationsById[String(postId)] = fallback; 1605 return fallback; 1606 } 1607 } 1608 1609 return null; 1610 } 1611 1612 function getCustomFieldValue(locationData, customFieldId) { 1613 if (!locationData || !Array.isArray(locationData.custom_fields)) { 1614 return null; 1615 } 1616 1617 const field = locationData.custom_fields.find( 1618 (customField) => String(customField.index) === String(customFieldId) 1619 ); 1620 1621 return field ? field.val : null; 1622 } 1623 1624 function normalizeFieldValues(fieldValue) { 1625 if (Array.isArray(fieldValue)) { 1626 return fieldValue.map((val) => val.toString()).filter((val) => val !== ""); 1627 } 1628 1629 if (typeof fieldValue === "string") { 1630 return fieldValue 1631 .split("|") 1632 .map((val) => val.trim()) 1633 .filter((val) => val !== ""); 1634 } 1635 1636 if (fieldValue !== null && typeof fieldValue !== "undefined") { 1637 const value = fieldValue.toString(); 1638 return value ? [value] : []; 1639 } 1640 1641 return []; 1642 } 1643 1644 function matchesCriterion(fieldValue, criterion) { 1645 if ( 1646 fieldValue === null || 1647 typeof fieldValue === "undefined" || 1648 (typeof fieldValue === "string" && fieldValue.trim() === "") || 1649 (Array.isArray(fieldValue) && fieldValue.length === 0) 1650 ) { 1651 return false; 1652 } 1653 1654 switch (criterion.type) { 1655 case "text": 1656 return fieldValue.toString().toLowerCase().includes(criterion.value); 1657 case "checkbox": { 1658 const values = normalizeFieldValues(fieldValue); 1659 const relation = criterion.relation || 'OR'; // Default to OR for backward compatibility 1660 1661 if (relation === 'AND') { 1662 // AND: All selected checkbox values must be present in the field values 1663 return criterion.values.length > 0 && criterion.values.every((val) => values.includes(val)); 1664 } else { 1665 // OR: At least one selected checkbox value must match (default behavior)
1666 return criterion.values.some((val) => values.includes(val)); 1667 } 1668 } 1669 case "radio": 1670 case "select": { 1671 const values = normalizeFieldValues(fieldValue); 1672 return values.includes(criterion.value); 1673 } 1674 default: 1675 return true; 1676 } 1677 } 1678 1679 function updateSearchtextAndCategoriesFilters() { 1680 // Read searchtext filter 1681 const markerFilterInput = document.getElementById("oum_filter_markers"); 1682 searchtextFilterValue = markerFilterInput ? markerFilterInput.value.toLowerCase() : ""; 1683 1684 // Read categories filter 1685 const categoryInputs = document.querySelectorAll(categoryFilterSelector); 1686 if (categoryInputs.length === 0) { 1687 // No category UI on this map (hidden overlay / hide_filterbox) â do not filter by category 1688 categoriesFilterSelection = null; 1689 } else { 1690 categoriesFilterSelection = new Set( 1691 Array.from(categoryInputs) 1692 .filter((input) => input.checked) 1693 .map((input) => input.value) 1694 ); 1695 } 1696 1697 applyFilters(); 1698 } 1699 1700 function setupFilterListEvents() { 1701 const filterControls = document.querySelector( 1702 ".open-user-map .oum-filter-controls" 1703 ); 1704 if (!filterControls) return; 1705 1706 const closeButton = filterControls.querySelector(".close-filter-list"); 1707 const toggle = filterControls.querySelector(".oum-filter-toggle"); 1708 const filterWrapper = filterControls.closest(".oum-map-filter-wrapper"); 1709 1710 // Close button always collapses the filter controls 1711 if (closeButton) { 1712 closeButton.addEventListener("click", function(e) { 1713 e.stopPropagation(); 1714 filterControls.classList.remove("active"); 1715 // Ensure use-collapse class is present so the toggle button becomes visible 1716 filterControls.classList.add("use-collapse"); 1717 }); 1718 } 1719 1720 // Setup click toggle for the toggle button (works regardless of initial use-collapse state) 1721 if (toggle) { 1722 toggle.addEventListener("click", function(e) { 1723 e.stopPropagation(); 1724 filterControls.classList.toggle("active"); 1725 }); 1726 } 1727 1728 // Close when clicking outside the filter wrapper (only if use-collapse is present) 1729 if (filterWrapper) { 1730 document.addEventListener("click", function(e) { 1731 if (filterControls.classList.contains("use-collapse") && !filterWrapper.contains(e.target)) { 1732 filterControls.classList.remove("active"); 1733 } 1734 }); 1735 } 1736 } 1737 1738 // Select all works for both the on-map overlay and the Advanced Filter section 1739 function setupToggleAll() { 1740 const categoryFilter = document.querySelector( 1741 ".open-user-map .oum-marker-categories-filter" 1742 ); 1743 if (!categoryFilter) return; 1744 1745 const toggleAllCheckbox = categoryFilter.querySelector( 1746 ".oum-toggle-all-checkbox" 1747 ); 1748 const categoryCheckboxes = categoryFilter.querySelectorAll( 1749 '[name="type"]' 1750 ); 1751 1752 if (!toggleAllCheckbox || categoryCheckboxes.length === 0) return; 1753 1754 function updateToggleAllState() { 1755 const checkedCount = Array.from(categoryCheckboxes).filter( 1756 (cb) => cb.checked 1757 ).length; 1758 const totalCount = categoryCheckboxes.length; 1759 1760 if (checkedCount === 0) { 1761 toggleAllCheckbox.checked = false; 1762 toggleAllCheckbox.indeterminate = false; 1763 } else if (checkedCount === totalCount) { 1764 toggleAllCheckbox.checked = true; 1765 toggleAllCheckbox.indeterminate = false; 1766 } else { 1767 toggleAllCheckbox.checked = false; 1768 toggleAllCheckbox.indeterminate = true; 1769 } 1770 } 1771 1772 function toggleAllCategories() { 1773 const checkedCount = Array.from(categoryCheckboxes).filter( 1774 (cb) => cb.checked 1775 ).length; 1776 const totalCount = categoryCheckboxes.length; 1777 const shouldCheck = checkedCount < totalCount; 1778 1779 categoryCheckboxes.forEach((checkbox) => { 1780 checkbox.checked = shouldCheck; 1781 }); 1782 1783 updateSearchtextAndCategoriesFilters(); 1784 } 1785 1786 toggleAllCheckbox.addEventListener("change", toggleAllCategories); 1787 1788 categoryCheckboxes.forEach((checkbox) => { 1789 checkbox.addEventListener("change", updateToggleAllState); 1790 }); 1791 1792 updateToggleAllState(); 1793 } 1794 1795 function resetCategoriesFilter() { 1796 const categoryInputs = document.querySelectorAll(categoryFilterSelector);
1797 categoryInputs.forEach((input) => { 1798 input.checked = true; 1799 }); 1800 1801 const toggleAllCheckbox = document.querySelector( 1802 ".open-user-map .oum-marker-categories-filter .oum-toggle-all-checkbox" 1803 ); 1804 if (toggleAllCheckbox) { 1805 toggleAllCheckbox.checked = true; 1806 toggleAllCheckbox.indeterminate = false; 1807 } 1808 1809 updateSearchtextAndCategoriesFilters(); 1810 } 1811 1812 function handleAutoOpenMarker(markerId) { 1813 const targetMarker = allMarkers.find((m) => m.options.post_id === markerId); 1814 if (targetMarker) { 1815 const openPopup = typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup; 1816 if (oum_enable_cluster) { 1817 if (targetMarker instanceof L.Marker && typeof markersLayer.zoomToShowLayer === "function") { 1818 setTimeout(() => { 1819 markersLayer.zoomToShowLayer(targetMarker, () => { 1820 if (openPopup) targetMarker.openPopup(); 1821 }); 1822 }, 500); 1823 } else if (targetMarker.getBounds) { 1824 map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] }); 1825 if (openPopup) targetMarker.openPopup(); 1826 } 1827 } else { 1828 if (targetMarker.getLatLng) {
1829 map.setView(targetMarker.getLatLng(), targetMarker.options.zoom); 1830 } else if (targetMarker.getBounds) { 1831 map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] }); 1832 } 1833 if (openPopup) targetMarker.openPopup(); 1834 } 1835 } 1836 } 1837 1838 // Public interface 1839 return { 1840 init: function (mapInstance) { 1841 map = mapInstance; 1842 initializeMarkersLayer(); 1843 markersLayer.addTo(map); 1844 vectorLayer.addTo(map); 1845 setupMarkerEvents(); 1846 setupFilterListEvents(); 1847 setupToggleAll(); 1848 1849 // Make layer globally available (for backward compatibility) 1850 window.oumMarkersLayer = markersLayer; 1851 window.oumAllMarkers = allMarkers; 1852 1853 // Dispatch markers initialized event 1854 document.dispatchEvent(new CustomEvent('oum:markers_initialized', { 1855 detail: { 1856 mapId: map._container.id, 1857 markersLayer: markersLayer 1858 } 1859 })); 1860 1861 return this; 1862 }, 1863 addMarkers: function (locations) { 1864 locations.forEach((location) => { 1865 const marker = createLocationLayer(location); 1866 allMarkers.push(marker); 1867 locationsById[String(location.post_id)] = location; 1868 }); 1869 1870 // Apply markers once so filters and clustering can batch-add visible markers. 1871 applyFilters(); 1872 1873 // After adding all markers, check if we need to auto-open one 1874 const POPUP_MARKER_ID = OUMUtils.getParameterByName("markerid"); 1875 if (POPUP_MARKER_ID) { 1876 handleAutoOpenMarker(POPUP_MARKER_ID); 1877 } 1878 }, 1879 updateSearchtextAndCategoriesFilters: updateSearchtextAndCategoriesFilters, 1880 resetCategoriesFilter: resetCategoriesFilter, 1881 getMarkersLayer: function () { 1882 return markersLayer; 1883 }, 1884 getAllMarkers: function () { 1885 return allMarkers; 1886 }, 1887 getAllLocations: function () { 1888 return Object.values(locationsById); 1889 }, 1890 setCustomfieldsFilter: function (criteria) { 1891 if (criteria && Object.keys(criteria).length > 0) { 1892 customfieldsFilterSelection = criteria; 1893 } else { 1894 customfieldsFilterSelection = null; 1895 } 1896 applyFilters(); 1897 }, 1898 getFilterState: function () { 1899 return { 1900 searchtext: searchtextFilterValue, 1901 categories: 1902 categoriesFilterSelection === null 1903 ? null 1904 : Array.from(categoriesFilterSelection), 1905 customfields: customfieldsFilterSelection 1906 }; 1907 }, 1908 getFilteredMarkersCount: function () { 1909 return visibleMarkersCount; 1910 }, 1911 /** Clear lazy-loaded popup HTML so the next open refetches (e.g. after vote count changes). */ 1912 invalidateBubbleHtmlCacheForPost: function (postId) { 1913 const id = String(postId); 1914 allMarkers.forEach((m) => { 1915 if (m && m.options && String(m.options.post_id) === id) { 1916 delete m._oumBubbleHtmlCache; 1917 m._oumBubbleFetchId = 0; 1918 } 1919 }); 1920 }, 1921 }; 1922})(); 1923 1924/** 1925 * Form Map Module - Handles all map-related functionality for the form 1926 */ 1927const OUMFormMap = (function () { 1928 // Private variables 1929 let formMap = null; 1930 let locationMarker = null; 1931 let markerIsVisible = false; 1932 let isAdjusting = false; 1933 let isInitialized = false; 1934 let currentGeometryType = "point"; 1935 let polylineDrawnItems = null; 1936 let polylineDrawControl = null; 1937 let polylineLayer = null; 1938 1939 // Private functions 1940 function initializeFormMap() { 1941 if (isInitialized) { 1942 return; 1943 } 1944 1945 formMap = L.map("mapGetLocation", { 1946 attributionControl: false, 1947 gestureHandling: true, 1948 minZoom: 1, 1949 zoomSnap: 0.01, // Allow fractional zoom levels (0.01 steps) 1950 zoomDelta: 1, // Zoom step size for controls 1951 fullscreenControl: oum_enable_fullscreen, 1952 fullscreenControlOptions: { 1953 position: "topleft", 1954 }, 1955 }); 1956 1957 // Make form map globally available (for backward compatibility) 1958 window.oumMap2 = formMap; 1959 1960 setupTileLayer(); 1961 setupControls(); 1962 setupMarker(); 1963 setupMapEvents(); 1964 setupPolylineDrawing(); 1965 setupLocationTypeControls(); 1966 1967 // Invalidate size to ensure accurate measurements before calculating bounds 1968 formMap.invalidateSize(); 1969 1970 // Always apply bounds to prevent showing repeated maps 1971 const boundsToUse = oum_enable_fixed_map_bounds 1972 ? sharedMapBounds 1973 : OUMConfig.defaults.map.bounds; 1974 1975 // Set the bounds 1976 formMap.setMaxBounds(boundsToUse); 1977 1978 // Calculate minimum zoom level based on bounds
1979 // When fixed bounds are enabled, use sharedMapBounds for accurate calculation 1980 // This is used both for setting minZoom and in the moveend event handler 1981 let maxVisibleBounds; 1982 if (oum_enable_fixed_map_bounds && sharedMapBounds) { 1983 // Invalidate size first to ensure accurate measurements 1984 formMap.invalidateSize(); 1985 maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds); 1986 // Set minimum zoom level to prevent users from zooming out too far 1987 formMap.setMinZoom(maxVisibleBounds); 1988 } else { 1989 maxVisibleBounds = formMap.getBoundsZoom(boundsToUse); 1990 } 1991 1992 // Add moveend event to enforce bounds 1993 formMap.on("moveend", function () { 1994 if (isAdjusting) return; 1995 isAdjusting = true; 1996 1997 const zoom = formMap.getZoom(); 1998 1999 // Only enforce bounds if we're zoomed in beyond the minimum zoom 2000 if (zoom > maxVisibleBounds) { 2001 const currentBounds = formMap.getBounds(); 2002 const currentCenter = formMap.getCenter(); 2003 2004 let needsAdjustment = false; 2005 let newLat = currentCenter.lat; 2006 let newLng = currentCenter.lng; 2007 2008 // Calculate current viewport dimensions 2009 const viewportHeight = currentBounds.getNorth() - currentBounds.getSouth(); 2010 const viewportWidth = currentBounds.getEast() - currentBounds.getWest(); 2011 2012 // Check and adjust latitude (north/south) 2013 if (currentBounds.getNorth() > boundsToUse.getNorth()) { 2014 newLat = boundsToUse.getNorth() - viewportHeight / 2; 2015 needsAdjustment = true; 2016 } else if (currentBounds.getSouth() < boundsToUse.getSouth()) { 2017 newLat = boundsToUse.getSouth() + viewportHeight / 2; 2018 needsAdjustment = true; 2019 } 2020 2021 // Check and adjust longitude (east/west) 2022 if (currentBounds.getEast() > boundsToUse.getEast()) { 2023 newLng = boundsToUse.getEast() - viewportWidth / 2; 2024 needsAdjustment = true; 2025 } else if (currentBounds.getWest() < boundsToUse.getWest()) { 2026 newLng = boundsToUse.getWest() + viewportWidth / 2; 2027 needsAdjustment = true; 2028 } 2029 2030 if (needsAdjustment) { 2031 formMap.setView([newLat, newLng], zoom, { animate: false }); 2032 } 2033 } 2034 2035 isAdjusting = false; 2036 }); 2037 2038 // Keep the hidden zoom input in sync with the current form map zoom. 2039 formMap.on("zoomend", function () { 2040 syncLocationZoomField(); 2041 }); 2042 2043 // Update minZoom when fixed bounds are enabled 2044 // This prevents users from zooming out too far 2045 updateMinZoom(); 2046 2047 isInitialized = true; 2048 } 2049 2050 function updateMinZoom() { 2051 if (!formMap) return; 2052 2053 // Recalculate and set minZoom when fixed bounds are enabled 2054 // This prevents users from zooming out beyond the defined bounds 2055 if (oum_enable_fixed_map_bounds && sharedMapBounds) { 2056 formMap.invalidateSize(); 2057 const maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds); 2058 formMap.setMinZoom(maxVisibleBounds); 2059 } 2060 } 2061 2062 function setupTileLayer() { 2063 // Default to OpenStreetMap if mapStyle is undefined 2064 const mapStyle = window.mapStyle || 'OpenFreeMap.bright'; 2065 2066 window.OUMMapStyles.addBaseLayer(formMap, mapStyle, { 2067 mapboxKey: OUMConfig.getTileProviderKey(), 2068 cartoKey: OUMConfig.getCartoTileProviderKey(), 2069 setupCustomImageLayer: setupCustomImageLayer, 2070 customImageHideTiles: window.oum_custom_image_hide_tiles, 2071 customImageBackgroundColor: window.oum_custom_image_background_color, 2072 }); 2073 } 2074 2075 function setupControls() { 2076 // Add searchbar: address 2077 const search = new GeoSearch.GeoSearchControl({ 2078 style: "bar", 2079 showMarker: false, 2080 provider: OUMUtils.initGeosearchProvider(), 2081 searchLabel: oum_searchaddress_label, 2082 updateMap: false, 2083 }); 2084 formMap.addControl(search); 2085 2086 // Add control: get current location 2087 if (oum_enable_currentlocation) { 2088 window.formMap_locate_process = L.control.locate({ 2089 flyTo: true, 2090 showPopup: false, 2091 }).addTo(formMap); 2092 } 2093 } 2094 2095 function setupMarker() { 2096 // Marker Icon 2097 let markerIcon = L.icon({ 2098 iconUrl: marker_icon_url, 2099 iconSize: [26, 41], 2100 iconAnchor: [13, 41], 2101 popupAnchor: [0, -25], 2102 shadowUrl: marker_shadow_url, 2103 shadowSize: [41, 41], 2104 shadowAnchor: [13, 41], 2105 }); 2106 2107 locationMarker = L.marker([0, 0], { 2108 icon: markerIcon, 2109 draggable: true, 2110 }); 2111 2112 // Make marker globally available (for backward compatibility) 2113 window.locationMarker = locationMarker; 2114 window.markerIsVisible = markerIsVisible; 2115 2116 // Event: drag marker 2117 locationMarker.on("dragend", function (e) { 2118 setLocationLatLng(e.target.getLatLng()); 2119 }); 2120 } 2121 2122 function setupMapEvents() { 2123 // Event: click on map to set marker OR location found 2124 formMap.on("click locationfound", function (e) { 2125 if (currentGeometryType !== "point") { 2126 return; 2127 } 2128 2129 let coords = e.latlng; 2130 locationMarker.setLatLng(coords); 2131 2132 if (!markerIsVisible) { 2133 locationMarker.addTo(formMap); 2134 markerIsVisible = true; 2135 window.markerIsVisible = true; 2136 } 2137 2138 setLocationLatLng(coords); 2139 }); 2140 2141 // Event: geosearch success 2142 formMap.on("geosearch/showlocation", handleGeosearchSuccess); 2143 } 2144 2145 function handleGeosearchSuccess(e) {
2146 let coords = e.marker._latlng; 2147 let label = e.location.label; 2148 const searchBar = document.querySelector(`#mapGetLocation .leaflet-geosearch-bar form`); 2149 2150 // Check against fixed map bounds if enabled, otherwise allow any location 2151 let isInBounds = true; // Default to true when fixed bounds is disabled 2152 if (oum_enable_fixed_map_bounds === 'on' && sharedMapBounds) { 2153 // Check if location is within the fixed map bounds (not just current viewport) 2154 isInBounds = sharedMapBounds.contains(coords); 2155 } 2156 2157 // Only restrict movement if fixed map bounds is enabled and location is outside those bounds 2158 if (!isInBounds && oum_enable_fixed_map_bounds === 'on') { 2159 console.log("This search result is out of reach."); 2160 if (searchBar) { 2161 searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)"; 2162 setTimeout(function () { 2163 searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)"; 2164 }, 2000); 2165 } 2166 } else { 2167 if (currentGeometryType !== "point") { 2168 handleValidGeosearchResult(e.location); 2169 return; 2170 } 2171
2172 // Location is within fixed bounds (or fixed bounds is disabled) - handle normally 2173 // Set coordinates and address immediately (before map animation) for instant feedback 2174 setLocationLatLng(coords, label); 2175 2176 // Set marker position 2177 locationMarker.setLatLng(coords); 2178 2179 if (!markerIsVisible) { 2180 locationMarker.addTo(formMap); 2181 markerIsVisible = true; 2182 window.markerIsVisible = true; 2183 } 2184 2185 // Fly to location (animated - happens after address is already set) 2186 handleValidGeosearchResult(e.location); 2187 } 2188 } 2189 2190 function handleValidGeosearchResult(location) { 2191 if (location.bounds !== null) { 2192 formMap.flyToBounds(location.bounds); 2193 } else if (location.raw.mapView) { 2194 formMap.flyToBounds([ 2195 [location.raw.mapView.south, location.raw.mapView.west], 2196 [location.raw.mapView.north, location.raw.mapView.east], 2197 ]); 2198 } else { 2199 formMap.flyTo([location.y, location.x], 17); 2200 } 2201 } 2202 2203 function setLocationLatLng(markerLatLng, address, skipReverseGeocode = false) { 2204 document.getElementById("oum_location_lat").value = markerLatLng.lat; 2205 document.getElementById("oum_location_lng").value = markerLatLng.lng; 2206 syncLocationZoomField(); 2207 2208 // Only perform reverse geocoding if both subtitle field and autofill are enabled 2209 if (!skipReverseGeocode && shouldPerformReverseGeocoding()) { 2210 reverseGeocode(markerLatLng.lat, markerLatLng.lng, address); 2211 } 2212 } 2213 2214 function clearMarker() { 2215 if (locationMarker && formMap) { 2216 locationMarker.remove(); 2217 markerIsVisible = false; 2218 window.markerIsVisible = false; 2219 } 2220 } 2221 2222 function setupLocationTypeControls() { 2223 const controls = document.querySelectorAll('input[name="oum_location_type_choice"]'); 2224 controls.forEach((control) => { 2225 control.addEventListener("change", function () { 2226 if (this.checked) { 2227 setGeometryType(this.value); 2228 } 2229 }); 2230 }); 2231 2232 const typeField = document.getElementById("oum_geometry_type"); 2233 setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false }); 2234 } 2235 2236 function setupPolylineDrawing() { 2237 if (!L.Control || !L.Control.Draw) { 2238 return; 2239 } 2240 2241 polylineDrawnItems = L.featureGroup().addTo(formMap); 2242 createPolylineDrawControl(); 2243 setupPolylineDrawingEvents(); 2244 } 2245 2246 function createPolylineDrawControl() { 2247 if (!formMap || !L.Control || !L.Control.Draw) { 2248 return; 2249 } 2250 2251 if (polylineDrawControl && polylineDrawControl._map) { 2252 formMap.removeControl(polylineDrawControl); 2253 } 2254 2255 polylineDrawControl = new L.Control.Draw({ 2256 draw: { 2257 polyline: currentGeometryType === "polyline" ? { 2258 shapeOptions: getPolylineStyle(), 2259 } : false, 2260 polygon: currentGeometryType === "polygon" ? { 2261 shapeOptions: getPolygonStyle(), 2262 } : false, 2263 rectangle: false, 2264 circle: false, 2265 circlemarker: false, 2266 marker: false, 2267 }, 2268 edit: { 2269 featureGroup: polylineDrawnItems, 2270 remove: true, 2271 }, 2272 }); 2273 } 2274 2275 function setupPolylineDrawingEvents() { 2276 formMap.on(L.Draw.Event.CREATED, function (event) { 2277 if (currentGeometryType === "point") { 2278 return; 2279 } 2280 2281 setPolylineLayer(event.layer); 2282 }); 2283 2284 formMap.on(L.Draw.Event.EDITED, function () { 2285 syncPolylineGeometryFields(); 2286 }); 2287 2288 formMap.on(L.Draw.Event.DELETED, function () { 2289 polylineLayer = null; 2290 syncPolylineGeometryFields(); 2291 }); 2292 } 2293 2294 function getPolylineStyle() { 2295 return { 2296 color: "#e82c71", 2297 weight: 4, 2298 opacity: 0.9, 2299 }; 2300 } 2301 2302 function getPolygonStyle() { 2303 return { 2304 ...getPolylineStyle(), 2305 fillColor: "#e82c71", 2306 fillOpacity: 0.25, 2307 }; 2308 } 2309 2310 function setGeometryType(type, options = {}) { 2311 currentGeometryType = type === "polyline" || type === "polygon" ? type : "point"; 2312 const autoStartDrawing = options.autoStartDrawing !== false; 2313 2314 cancelActiveDrawTool(); 2315 2316 const addLocationEl = document.querySelector(".add-location"); 2317 if (addLocationEl) { 2318 addLocationEl.setAttribute("data-oum-geometry-type", currentGeometryType); 2319 } 2320 2321 const typeField = document.getElementById("oum_geometry_type"); 2322 if (typeField) { 2323 typeField.value = currentGeometryType; 2324 } 2325 2326 const choice = document.querySelector(`input[name="oum_location_type_choice"][value="${currentGeometryType}"]`); 2327 if (choice) { 2328 choice.checked = true; 2329 } 2330 2331 document.querySelectorAll("[data-oum-location-type-card]").forEach((card) => { 2332 card.classList.toggle("is-selected", card.getAttribute("data-oum-location-type-card") === currentGeometryType); 2333 }); 2334 2335 updateLocationTypeCategoryUi(); 2336 2337 if (currentGeometryType === "polyline") { 2338 clearMarker(); 2339 createPolylineDrawControl(); 2340 addPolylineControl(); 2341 if (autoStartDrawing) { 2342 startActiveDrawTool(); 2343 } 2344 } else if (currentGeometryType === "polygon") { 2345 clearMarker(); 2346 createPolylineDrawControl(); 2347 addPolylineControl(); 2348 if (autoStartDrawing) { 2349 startActiveDrawTool(); 2350 } 2351 } else { 2352 removePolylineControl(); 2353 clearPolyline(); 2354 } 2355 2356 syncPolylineGeometryFields(); 2357 } 2358 2359 function updateLocationTypeCategoryUi() { 2360 const categoryLabelKey = currentGeometryType === "polygon" 2361 ? "polygonLabel" 2362 : (currentGeometryType === "polyline" ? "polylineLabel" : "pointLabel"); 2363 const categoryTitles = document.querySelectorAll("[data-oum-category-title]"); 2364
2365 categoryTitles.forEach((titleEl) => { 2366 const label = titleEl.dataset[categoryLabelKey] || titleEl.dataset.pointLabel || titleEl.textContent; 2367 titleEl.textContent = label; 2368 }); 2369 2370 document.querySelectorAll(".oum-category-dropdown-display").forEach((display) => { 2371 display.setAttribute("data-oum-geometry-type", currentGeometryType); 2372 }); 2373 2374 document.querySelectorAll(".oum-marker-category-option").forEach((option) => { 2375 const categoryType = option.getAttribute("data-category-type") || "point"; 2376 const isMatchingType = categoryType === currentGeometryType; 2377 const input = option.querySelector('input[name="oum_marker_icon[]"]'); 2378 2379 option.hidden = !isMatchingType; 2380 2381 if (input) { 2382 input.disabled = !isMatchingType; 2383 if (!isMatchingType) { 2384 input.checked = false; 2385 } 2386 } 2387 }); 2388 2389 document.querySelectorAll('select#oum_marker_icon option[data-category-type]').forEach((option) => { 2390 const categoryType = option.getAttribute("data-category-type") || "point"; 2391 const isMatchingType = categoryType === currentGeometryType; 2392 2393 option.hidden = !isMatchingType; 2394 option.disabled = !isMatchingType; 2395 if (!isMatchingType && option.selected) { 2396 option.selected = false; 2397 } 2398 }); 2399 2400 document.querySelectorAll("select#oum_marker_icon").forEach((select) => { 2401 const selectedOption = select.options[select.selectedIndex]; 2402 if (selectedOption && selectedOption.disabled) { 2403 select.value = ""; 2404 } 2405 select.dispatchEvent(new Event("change")); 2406 }); 2407 } 2408 2409 function startActiveDrawTool(attempt = 0) { 2410 if (!formMap) { 2411 return; 2412 } 2413 2414 window.setTimeout(() => { 2415 if (!formMap._loaded) { 2416 if (attempt < 10) { 2417 startActiveDrawTool(attempt + 1); 2418 } 2419 return; 2420 } 2421 2422 const mapContainer = formMap.getContainer(); 2423 if (!mapContainer) { 2424 return; 2425 } 2426 2427 const selector = currentGeometryType === "polygon" 2428 ? ".leaflet-draw-draw-polygon" 2429 : ".leaflet-draw-draw-polyline"; 2430 const drawButton = mapContainer.querySelector(selector); 2431 2432 if ( 2433 drawButton && 2434 !drawButton.classList.contains("leaflet-disabled") && 2435 !drawButton.classList.contains("leaflet-draw-toolbar-button-enabled") 2436 ) { 2437 drawButton.click(); 2438 } 2439 }, 0); 2440 } 2441 2442 function startDrawToolForCurrentType() { 2443 if (currentGeometryType === "polyline" || currentGeometryType === "polygon") { 2444 startActiveDrawTool(); 2445 } 2446 } 2447 2448 function startEditLayersTool() { 2449 if (!formMap || !polylineLayer) { 2450 return; 2451 } 2452 2453 window.setTimeout(() => { 2454 const mapContainer = formMap.getContainer(); 2455 if (!mapContainer) { 2456 return; 2457 } 2458 2459 const editButton = mapContainer.querySelector(".leaflet-draw-edit-edit"); 2460 const editIsActive = editButton && editButton.classList.contains("leaflet-draw-toolbar-button-enabled"); 2461 const editIsDisabled = editButton && editButton.classList.contains("leaflet-disabled"); 2462 2463 if (editButton && !editIsActive && !editIsDisabled) { 2464 editButton.click(); 2465 } 2466 }, 0); 2467 } 2468 2469 function cancelActiveDrawTool() { 2470 if (!formMap) { 2471 return; 2472 } 2473 2474 const mapContainer = formMap.getContainer(); 2475 if (!mapContainer) { 2476 return; 2477 } 2478 2479 const cancelButton = mapContainer.querySelector(".leaflet-draw-actions a[title='Cancel drawing'], .leaflet-draw-actions a[title='Cancel']"); 2480 if (cancelButton) { 2481 cancelButton.click(); 2482 } 2483 } 2484 2485 function addPolylineControl() { 2486 if (polylineDrawControl && formMap && !polylineDrawControl._map) { 2487 formMap.addControl(polylineDrawControl); 2488 } 2489 } 2490 2491 function removePolylineControl() { 2492 if (polylineDrawControl && formMap && polylineDrawControl._map) { 2493 formMap.removeControl(polylineDrawControl); 2494 } 2495 } 2496 2497 function setPolylineLayer(layer) { 2498 if (!polylineDrawnItems || !layer) { 2499 return; 2500 } 2501 2502 clearPolyline(); 2503 polylineLayer = layer; 2504 if (polylineLayer.setStyle) { 2505 polylineLayer.setStyle(currentGeometryType === "polygon" ? getPolygonStyle() : getPolylineStyle()); 2506 } 2507 polylineDrawnItems.addLayer(polylineLayer); 2508 syncPolylineGeometryFields(); 2509 } 2510 2511 function clearPolyline() { 2512 if (polylineDrawnItems) { 2513 polylineDrawnItems.clearLayers(); 2514 } 2515 polylineLayer = null; 2516 } 2517 2518 function setPolylineGeometry(geometry) { 2519 if (!geometry || !Array.isArray(geometry.coordinates) || (geometry.type !== "LineString" && geometry.type !== "Polygon")) { 2520 clearPolyline(); 2521 syncPolylineGeometryFields(); 2522 return; 2523 } 2524 2525 const rawCoordinates = geometry.type === "Polygon" && Array.isArray(geometry.coordinates[0]) 2526 ? geometry.coordinates[0] 2527 : geometry.coordinates; 2528 const latLngs = rawCoordinates 2529 .filter((coordinate) => Array.isArray(coordinate) && coordinate.length >= 2) 2530 .map((coordinate) => L.latLng(Number(coordinate[1]), Number(coordinate[0]))); 2531 2532 if (latLngs.length < 2 || (geometry.type === "Polygon" && latLngs.length < 4)) { 2533 clearPolyline(); 2534 syncPolylineGeometryFields(); 2535 return; 2536 } 2537 2538 setGeometryType(geometry.type === "Polygon" ? "polygon" : "polyline", { autoStartDrawing: false }); 2539 setPolylineLayer(geometry.type === "Polygon" ? L.polygon(latLngs, getPolygonStyle()) : L.polyline(latLngs, getPolylineStyle())); 2540 fitPolylineBounds(); 2541 } 2542 2543 function fitPolylineBounds() { 2544 if (!formMap || !polylineLayer || !polylineLayer.getBounds) { 2545 return; 2546 } 2547 2548 const bounds = polylineLayer.getBounds(); 2549 if (bounds && bounds.isValid()) { 2550 formMap.fitBounds(bounds, { padding: [20, 20] }); 2551 } 2552 } 2553 2554 function syncPolylineGeometryFields() { 2555 const geometryField = document.getElementById("oum_location_geometry"); 2556 2557 if (!geometryField) { 2558 return; 2559 } 2560 2561 if (currentGeometryType === "point" || !polylineLayer) { 2562 geometryField.value = ""; 2563 return; 2564 } 2565 2566 const geometry = polylineLayer.toGeoJSON().geometry; 2567 geometryField.value = JSON.stringify(geometry); 2568 2569 const bounds = polylineLayer.getBounds(); 2570 if (bounds && bounds.isValid()) { 2571 setLocationLatLng(bounds.getCenter(), "", true); 2572 } 2573 } 2574 2575 function syncLocationZoomField() { 2576 const zoomField = document.getElementById("oum_location_zoom"); 2577 if (!zoomField || !formMap) { 2578 return; 2579 } 2580 zoomField.value = formMap.getZoom(); 2581 } 2582 2583 /** 2584 * Check if reverse geocoding should be performed 2585 * Requires both subtitle field and autofill to be enabled 2586 * @returns {boolean} 2587 */ 2588 function shouldPerformReverseGeocoding() { 2589 // Check if subtitle field is enabled (use window prefix for safety) 2590 const subtitleEnabled = typeof window.oum_enable_address !== 'undefined' && window.oum_enable_address === 'on'; 2591 2592 // Check if autofill is enabled (use window prefix for safety) 2593 const autofillEnabled = typeof window.oum_enable_address_autofill !== 'undefined' && window.oum_enable_address_autofill === 'on'; 2594 2595 return subtitleEnabled && autofillEnabled; 2596 } 2597 2598 /** 2599 * Format address from Nominatim API response 2600 * @param {Object} addressData - Address data from Nominatim API 2601 * @param {string} displayName - Fallback display name 2602 * @returns {string} 2603 */
2604 function formatAddressFromResponse(addressData, displayName) { 2605 const addressParts = []; 2606 2607 // Build address from most specific to least specific 2608 if (addressData.house_number) addressParts.push(addressData.house_number); 2609 if (addressData.road) addressParts.push(addressData.road); 2610 if (addressData.suburb) addressParts.push(addressData.suburb); 2611 if (addressData.city || addressData.town || addressData.village) { 2612 addressParts.push(addressData.city || addressData.town || addressData.village); 2613 } 2614 if (addressData.postcode) addressParts.push(addressData.postcode); 2615 if (addressData.country) addressParts.push(addressData.country); 2616 2617 return addressParts.length > 0 2618 ? addressParts.join(', ') 2619 : displayName || 'Address not found'; 2620 } 2621 2622 /** 2623 * Reverse geocode coordinates to get address 2624 * Uses OpenStreetMap Nominatim API (free, no API key required) 2625 * @param {number} lat - Latitude 2626 * @param {number} lng - Longitude 2627 * @param {string} [providedAddress] - Optional address to use directly (e.g., from geosearch result) 2628 */ 2629 function reverseGeocode(lat, lng, providedAddress) { 2630 // If address is already provided (e.g., from geosearch), use it directly 2631 if (providedAddress && providedAddress.trim()) { 2632 handleAddressResult(providedAddress.trim()); 2633 return; 2634 } 2635 2636 // Validate coordinates 2637 if (!OUMUtils.validateCoordinates(lat, lng)) { 2638 console.warn('Open User Map: Invalid coordinates for reverse geocoding'); 2639 return; 2640 } 2641 2642 // Use Nominatim reverse geocoding API 2643 const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`; 2644 2645 fetch(url, { 2646 headers: { 2647 'User-Agent': 'OpenUserMap/1.0' // Required by Nominatim 2648 } 2649 }) 2650 .then(response => { 2651 if (!response.ok) { 2652 throw new Error(`HTTP error! status: ${response.status}`); 2653 } 2654 return response.json(); 2655 }) 2656 .then(data => { 2657 if (data && data.address) { 2658 const address = formatAddressFromResponse(data.address, data.display_name); 2659 handleAddressResult(address); 2660 } else { 2661 console.log('Open User Map: No address found for coordinates:', lat, lng); 2662 } 2663 }) 2664 .catch(error => { 2665 console.warn('Open User Map: Reverse geocoding error:', error); 2666 }); 2667 } 2668 2669 /** 2670 * Handle the address result - log it and optionally fill subtitle field 2671 * @param {string} address - The formatted address 2672 */ 2673 function handleAddressResult(address) { 2674 if (!address || typeof address !== 'string') { 2675 return; 2676 } 2677 2678 console.log('Open User Map: Address:', address); 2679 2680 // Auto-fill subtitle field if enabled (always update when address changes) 2681 if (shouldPerformReverseGeocoding()) { 2682 const subtitleField = document.getElementById('oum_location_address'); 2683 if (subtitleField) { 2684 subtitleField.value = address; 2685 } 2686 } 2687 } 2688 2689 // Helper function to setup custom image layer 2690 function setupCustomImageLayer() { 2691 // Check if we have an image URL and bounds 2692 if (typeof window.oum_custom_image_url !== 'undefined' && window.oum_custom_image_url && 2693 typeof window.oum_custom_image_bounds !== 'undefined' && window.oum_custom_image_bounds) { 2694 2695 // Check if the uploaded file is an SVG 2696 const isSVG = window.oum_custom_image_url.toLowerCase().includes('.svg'); 2697 2698 if (isSVG) { 2699 // Handle SVG file - fetch and render as DOM elements 2700 setupSVGFromFile(); 2701 } else { 2702 // Handle regular image file 2703 setupImageOverlay(); 2704 } 2705 } else { 2706 } 2707 } 2708 2709 // Helper function to setup SVG from uploaded file 2710 function setupSVGFromFile() { 2711 try { 2712 // Get bounds data (now properly handled as object) 2713 const bounds = window.oum_custom_image_bounds; 2714 2715 // Validate bounds 2716 if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' || 2717 typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' || 2718 bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') { 2719 console.warn('Open User Map: Invalid or empty bounds data, skipping SVG file layer'); 2720 return; 2721 } 2722 2723 2724 // Fetch the SVG file and render it 2725 fetch(window.oum_custom_image_url) 2726 .then(response => response.text()) 2727 .then(svgText => { 2728 // Create SVG element from the fetched content 2729 const svgElement = createSVGElement(svgText); 2730 if (!svgElement) { 2731 console.warn('Open User Map: Cannot create SVG layer from file - invalid SVG element'); 2732 return; 2733 } 2734 2735 // Create a custom SVG layer 2736 const svgLayer = L.svgOverlay(svgElement, [ 2737 [bounds.north, bounds.west], // Southwest corner 2738 [bounds.south, bounds.east] // Northeast corner 2739 ], { 2740 opacity: 1.0, 2741 interactive: true 2742 }); 2743 2744 svgLayer.addTo(formMap); 2745 2746 2747 // Store reference for potential removal 2748 window.oumCustomSVGLayer = svgLayer; 2749 2750 console.log('Open User Map: Custom SVG file layer added successfully'); 2751 }) 2752 .catch(error => { 2753 console.warn('Open User Map: Error fetching SVG file:', error); 2754 }); 2755 2756 } catch (error) { 2757 console.warn('Open User Map: Error setting up custom SVG file layer:', error); 2758 } 2759 } 2760 2761 // Helper function to setup regular image overlay 2762 function setupImageOverlay() { 2763 try { 2764 // Get bounds data (now properly handled as object) 2765 const bounds = window.oum_custom_image_bounds; 2766 2767 // Validate bounds 2768 if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' || 2769 typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' || 2770 bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') { 2771 console.warn('Open User Map: Invalid or empty bounds data, skipping image layer'); 2772 return; 2773 } 2774 2775 2776 // Create image overlay 2777 const imageOverlay = L.imageOverlay(window.oum_custom_image_url, [ 2778 [bounds.north, bounds.west], // Southwest corner 2779 [bounds.south, bounds.east] // Northeast corner 2780 ], { 2781 opacity: 1.0, 2782 interactive: true 2783 }); 2784 2785 imageOverlay.addTo(formMap); 2786 2787 2788 // Store reference for potential removal 2789 window.oumCustomImageLayer = imageOverlay; 2790 2791 console.log('Open User Map: Custom image layer added successfully'); 2792 2793 } catch (error) { 2794 console.warn('Open User Map: Error setting up custom image layer:', error); 2795 } 2796 } 2797 2798 // Helper function to create SVG element from text 2799 function createSVGElement(svgText) { 2800 // Create a temporary div to parse the SVG 2801 const tempDiv = document.createElement('div'); 2802 tempDiv.innerHTML = svgText; 2803 const svgElement = tempDiv.querySelector('svg'); 2804 2805 if (!svgElement) { 2806 console.warn('Open User Map: No valid SVG element found in SVG text'); 2807 return null; 2808 } 2809 2810 // Preserve the original viewBox if it exists 2811 // If missing, try to create it from width/height attributes 2812 if (!svgElement.getAttribute('viewBox')) { 2813 const width = svgElement.getAttribute('width'); 2814 const height = svgElement.getAttribute('height'); 2815 if (width && height) { 2816 // Remove units if present (e.g., "1580px" -> "1580") 2817 const widthNum = parseFloat(width); 2818 const heightNum = parseFloat(height); 2819 if (!isNaN(widthNum) && !isNaN(heightNum)) { 2820 svgElement.setAttribute('viewBox', `0 0 ${widthNum} ${heightNum}`); 2821 } else { 2822 svgElement.setAttribute('viewBox', '0 0 1000 1200'); 2823 } 2824 } else { 2825 svgElement.setAttribute('viewBox', '0 0 1000 1200'); 2826 } 2827 } 2828 2829 // Ensure the SVG has proper styling for overlay 2830 svgElement.style.width = '100%'; 2831 svgElement.style.height = '100%'; 2832 svgElement.style.display = 'block'; 2833 2834 // Ensure the SVG fills the entire bounds area to prevent cropping 2835 svgElement.setAttribute('preserveAspectRatio', 'none'); 2836 2837 console.log('Open User Map: SVG element created successfully:', svgElement); 2838 2839 return svgElement; 2840 } 2841 2842 // Public interface 2843 return { 2844 init: function() { 2845 if (!document.getElementById("mapGetLocation")) { 2846 return; 2847 } 2848 initializeFormMap(); 2849 },
2850 setLocation: function(lat, lng) { 2851 setGeometryType("point"); 2852 if (!locationMarker) return; 2853 locationMarker.setLatLng([lat, lng]); 2854 if (!markerIsVisible) { 2855 locationMarker.addTo(formMap); 2856 markerIsVisible = true; 2857 window.markerIsVisible = true; 2858 } 2859 setLocationLatLng({lat, lng}); 2860 }, 2861 clearMarker: function() { 2862 clearMarker(); 2863 }, 2864 setGeometryType: setGeometryType, 2865 updateLocationTypeCategoryUi: updateLocationTypeCategoryUi, 2866 setPolylineGeometry: setPolylineGeometry, 2867 fitPolylineBounds: fitPolylineBounds, 2868 startDrawToolForCurrentType: startDrawToolForCurrentType, 2869 startEditLayersTool: startEditLayersTool, 2870 clearPolyline: clearPolyline, 2871 syncGeometryFields: syncPolylineGeometryFields, 2872 invalidateSize: function() { 2873 if (formMap) { 2874 formMap.invalidateSize(); 2875 } 2876 }, 2877 updateMinZoom: function() { 2878 updateMinZoom(); 2879 }, 2880 getMap: function() { 2881 return formMap; 2882 }, 2883 setView: function(lat, lng, zoom) { 2884 if (formMap) { 2885 formMap.setView([lat, lng], zoom); 2886 syncLocationZoomField(); 2887 } 2888 },
2889 setViewToMatchMainMap: function(mainMap) { 2890 if (!formMap || !mainMap) return; 2891 2892 // Invalidate size to ensure accurate measurements 2893 formMap.invalidateSize(); 2894 2895 // Get the main map's bounds (the geographic area currently visible) 2896 const mainBounds = mainMap.getBounds(); 2897 const mainCenter = mainMap.getCenter(); 2898 const mainZoom = mainMap.getZoom(); 2899 2900 // Get form map container size 2901 const formMapContainer = formMap.getContainer(); 2902 const formMapWidth = formMapContainer.offsetWidth || 520; 2903 const formMapHeight = formMapContainer.offsetHeight || 294; 2904 2905 // Get main map container size 2906 const mainMapContainer = mainMap.getContainer(); 2907 const mainMapWidth = mainMapContainer.offsetWidth || 520; 2908 const mainMapHeight = mainMapContainer.offsetHeight || 294; 2909 2910 // Calculate aspect ratios 2911 const mainAspectRatio = mainMapWidth / mainMapHeight; 2912 const formAspectRatio = formMapWidth / formMapHeight; 2913 const aspectRatioDifference = Math.abs(mainAspectRatio - formAspectRatio); 2914 2915 let targetZoom; 2916 2917 // If aspect ratios match, use ratio-based zoom calculation 2918 if (aspectRatioDifference < 0.01) { 2919 // Calculate zoom adjustment based on size ratio 2920 const widthRatio = formMapWidth / mainMapWidth; 2921 const zoomAdjustment = Math.log2(widthRatio); 2922 targetZoom = mainZoom + zoomAdjustment; 2923 } else { 2924 // Different aspect ratios - use getBoundsZoom to fit the main map's bounds 2925 targetZoom = formMap.getBoundsZoom(mainBounds, false); 2926 } 2927 2928 // Set view with calculated zoom to show the same geographic area 2929 formMap.setView([mainCenter.lat, mainCenter.lng], targetZoom, { 2930 animate: false 2931 }); 2932 } 2933 }; 2934})(); 2935 2936/** 2937 * Form Controller Module - Handles all form-related functionality 2938 */ 2939const OUMFormController = (function () { 2940 // Private variables 2941 let isEditMode = false; 2942 let currentLocationId = null; 2943 let selectedFiles = []; 2944 2945 // Private functions 2946 function showFormMessage(type, headline, message, buttonText = null, buttonCallback = null) { 2947 const form = document.getElementById('oum_add_location'); 2948 const errorDiv = document.getElementById('oum_add_location_error'); 2949 const thankyouDiv = document.getElementById('oum_add_location_thankyou'); 2950 2951 if (!form || !errorDiv || !thankyouDiv) { 2952 console.error('Required form elements not found'); 2953 return; 2954 } 2955 2956 // Hide form and error 2957 form.style.display = 'none'; 2958 errorDiv.style.display = 'none'; 2959 2960 // Update thank you message 2961 const headlineEl = thankyouDiv.querySelector('h3'); 2962 const messageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text'); 2963 const buttonEl = thankyouDiv.querySelector('button'); 2964 2965 if (!headlineEl || !messageEl || !buttonEl) { 2966 // Create elements if they don't exist 2967 if (!headlineEl) { 2968 const newHeadline = document.createElement('h3'); 2969 thankyouDiv.appendChild(newHeadline); 2970 } 2971 if (!messageEl) { 2972 const newMessage = document.createElement('p'); 2973 newMessage.className = 'oum-add-location-thankyou-text'; 2974 thankyouDiv.appendChild(newMessage); 2975 } 2976 if (!buttonEl) { 2977 const newButton = document.createElement('button'); 2978 thankyouDiv.appendChild(newButton); 2979 } 2980 } 2981 2982 // Get elements again (they should exist now) 2983 const finalHeadlineEl = thankyouDiv.querySelector('h3'); 2984 const finalMessageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text'); 2985 const finalButtonEl = thankyouDiv.querySelector('button'); 2986 2987 // Add specific class for delete confirmation 2988 thankyouDiv.className = type === 'confirm-delete' ? 'oum-delete-confirmation' : ''; 2989 2990 if (finalHeadlineEl) finalHeadlineEl.textContent = headline || ''; 2991 if (finalMessageEl) finalMessageEl.textContent = message || ''; 2992 2993 // Handle button 2994 if (finalButtonEl) { 2995 if (buttonText && buttonCallback) { 2996 finalButtonEl.textContent = buttonText; 2997 finalButtonEl.onclick = buttonCallback; 2998 finalButtonEl.style.display = 'inline-block'; 2999 } else { 3000 finalButtonEl.style.display = 'none'; 3001 } 3002 } 3003 3004 thankyouDiv.style.display = 'block'; 3005 } 3006 3007 function setupDeleteButton() { 3008 const deleteBtn = document.getElementById('oum_delete_location_btn'); 3009 if (deleteBtn) { 3010 deleteBtn.addEventListener('click', function(e) { 3011 e.preventDefault(); 3012 3013 // Show confirmation using the message system 3014 showFormMessage( 3015 'confirm-delete', 3016 oum_custom_strings.delete_location, 3017 oum_custom_strings.delete_location_message, 3018 oum_custom_strings.delete_location_button, 3019 function() { 3020 // Set delete flag 3021 document.getElementById('oum_delete_location').value = 'true'; 3022 3023 // Get the form 3024 const form = document.getElementById('oum_add_location'); 3025 const formData = new FormData(form); 3026 formData.append('action', 'oum_add_location_from_frontend'); 3027 3028 // Submit via AJAX 3029 jQuery.ajax({ 3030 type: 'POST', 3031 url: oum_ajax.ajaxurl, 3032 cache: false, 3033 contentType: false, 3034 processData: false, 3035 data: formData, 3036 success: function(response) { 3037 if (response.success) { 3038 showFormMessage( 3039 'success', 3040 oum_custom_strings.location_deleted, 3041 oum_custom_strings.delete_success, 3042 oum_custom_strings.close_and_refresh, 3043 function() { 3044 window.location.reload(); 3045 } 3046 ); 3047 } else { 3048 oumShowError(response.data); 3049 } 3050 }, 3051 error: function(XMLHttpRequest, textStatus, errorThrown) { 3052 console.log(errorThrown); 3053 oumShowError([{message: oum_custom_strings.delete_error}]); 3054 } 3055 }); 3056 } 3057 ); 3058 }); 3059 } 3060 } 3061 3062 function setupFormEvents() { 3063 // Event: click on "+ Add Location" button 3064 const addLocationBtn = document.getElementById("open-add-location-overlay"); 3065 if (addLocationBtn) { 3066 addLocationBtn.addEventListener("click", handleAddLocationClick); 3067 } 3068 3069 // Event: click on "Edit Location" button 3070 document.addEventListener('click', function(e) { 3071 if (e.target && e.target.classList.contains('edit-location-button')) { 3072 e.preventDefault(); 3073 const locationId = e.target.getAttribute('data-post-id'); 3074 const location = window.oum_all_locations.find(loc => loc.post_id === locationId); 3075 3076 if (location) { 3077 resetForm(); 3078 openForm(location); 3079 } 3080 } 3081 }); 3082 3083 setupCloseEvents(); 3084 setupNotificationEvents(); 3085 setupMediaEvents(); 3086 } 3087 3088 /** 3089 * Request a fresh form nonce so cached pages stay valid. 3090 * 3091 * We refresh when the overlay opens, ensuring the submission 3092 * uses a nonce generated in the current time window. 3093 */ 3094 function refreshLocationNonce() { 3095 const formEl = document.getElementById('oum_add_location'); 3096 if (!formEl || typeof oum_ajax === 'undefined') { 3097 return Promise.resolve(); 3098 } 3099 3100 const nonceField = formEl.querySelector('input[name="oum_location_nonce"]'); 3101 if (!nonceField || !oum_ajax.ajaxurl) { 3102 return Promise.resolve(); 3103 } 3104 3105 const action = oum_ajax.refresh_nonce_action || 'oum_refresh_location_nonce'; 3106 3107 return jQuery.ajax({ 3108 type: 'POST', 3109 url: oum_ajax.ajaxurl, 3110 dataType: 'json', 3111 data: { action }, 3112 }).done(function(response) { 3113 if (response && response.success && response.data && response.data.nonce) { 3114 nonceField.value = response.data.nonce; 3115 } 3116 }).fail(function() { 3117 // Keep backward compatibility: fall back to the existing nonce if refresh fails. 3118 console.warn('Open User Map: Unable to refresh nonce.'); 3119 }); 3120 } 3121 3122 function handleAddLocationClick() { 3123 resetForm(); 3124 openForm(); 3125 } 3126 3127 function openForm(location = null) {
3128 // Refresh nonce on every open to avoid cached/expired tokens. 3129 refreshLocationNonce(); 3130 document.querySelector(".add-location").classList.add("active"); 3131 document.body.classList.add("oum-add-location-opened"); 3132 3133 setTimeout(function () { 3134 // Initialize map if needed 3135 OUMFormMap.init(); 3136 3137 // Update minZoom after form opens to ensure it's set correctly when fixed bounds are enabled 3138 // Use a small delay to ensure the map container is properly sized 3139 setTimeout(function() { 3140 OUMFormMap.updateMinZoom(); 3141 }, 100); 3142 3143 // Apply main map's aspect ratio to form map 3144 const applyAspectRatioAndView = () => { 3145 const mainMapEl = document.querySelector(`#${map_el}`); 3146 if (mainMapEl) { 3147 const mainMap = window.oumMap; 3148 if (mainMap) { 3149 const mainMapContainer = mainMap.getContainer(); 3150 const mainMapWidth = mainMapContainer.offsetWidth; 3151 const mainMapHeight = mainMapContainer.offsetHeight; 3152 3153 if (mainMapWidth && mainMapHeight) { 3154 // Calculate aspect ratio from main map 3155 const aspectRatio = mainMapWidth / mainMapHeight; 3156 3157 // Apply aspect ratio to form map container 3158 const formMapWrap = document.querySelector('.add-location .location-overlay-content .map-wrap'); 3159 if (formMapWrap) { 3160 formMapWrap.style.aspectRatio = aspectRatio.toString(); 3161 formMapWrap.style.height = 'auto'; 3162 } 3163 } 3164 } 3165 } 3166 3167 if (location) { 3168 populateForm(location); 3169 } else { 3170 // Set view to match main map's geographic bounds 3171 const mainMapEl = document.querySelector(`#${map_el}`); 3172 if (mainMapEl) { 3173 const mainMap = window.oumMap; 3174 // Use the new method that calculates zoom based on form map size 3175 // This ensures the same geographic area is visible regardless of map size differences 3176 OUMFormMap.setViewToMatchMainMap(mainMap); 3177 } 3178 } 3179 }; 3180 3181 // Wait for container to resize after applying aspect ratio 3182 applyAspectRatioAndView(); 3183 requestAnimationFrame(applyAspectRatioAndView); 3184 3185 // Add a separate timeout for invalidateSize to ensure accurate measurements 3186 setTimeout(() => { 3187 OUMFormMap.invalidateSize(); 3188 // Re-apply the view after size invalidation to ensure correct zoom 3189 if (location && (location.geometry_type === "polyline" || location.geometry_type === "polygon") && location.geometry) { 3190 OUMFormMap.fitPolylineBounds(); 3191 } else if (!location) { 3192 const mainMapEl = document.querySelector(`#${map_el}`); 3193 if (mainMapEl) { 3194 const mainMap = window.oumMap; 3195 OUMFormMap.setViewToMatchMainMap(mainMap); 3196 } 3197 OUMFormMap.startDrawToolForCurrentType(); 3198 } 3199 }, 200); 3200 }, 150); 3201 } 3202 3203 function setupCloseEvents() { 3204 const closeBtn = document.getElementById("close-add-location-overlay"); 3205 if (!closeBtn) return; 3206 3207 // Close button click 3208 closeBtn.addEventListener("click", closeForm); 3209 3210 // ESC key 3211 document.addEventListener("keydown", function(evt) { 3212 evt = evt || window.event; 3213 if (evt.key === "Escape" && document.getElementById("add-location-overlay").classList.contains("active")) { 3214 closeForm(); 3215 } 3216 }); 3217 3218 // Backdrop click 3219 document.getElementById("add-location-overlay").addEventListener("click", function(event) { 3220 if (event.target === this) { 3221 closeForm(); 3222 } 3223 }); 3224 } 3225 3226 function closeForm() { 3227 const addLocationOverlay = document.getElementById("add-location-overlay"); 3228 if (addLocationOverlay) { 3229 addLocationOverlay.classList.remove("active"); 3230 } 3231 3232 // Stop locate process 3233 if (window.formMap_locate_process) { 3234 window.formMap_locate_process.stop(); 3235 } 3236 3237 // Allow body scrolling 3238 document.querySelector("body").classList.remove("oum-add-location-opened"); 3239 3240 // Reset form and clear marker 3241 resetForm(); 3242 OUMFormMap.clearMarker(); 3243 } 3244 3245 function setupNotificationEvents() { 3246 const notificationCheckbox = document.getElementById("oum_location_notification"); 3247 if (notificationCheckbox) { 3248 notificationCheckbox.addEventListener("change", function() { 3249 const authorFields = document.getElementById("oum_author"); 3250 const nameField = document.getElementById("oum_location_author_name"); 3251 const emailField = document.getElementById("oum_location_author_email"); 3252 3253 if (this.checked) { 3254 authorFields.classList.add("active"); 3255 nameField.required = true; 3256 emailField.required = true; 3257 } else { 3258 authorFields.classList.remove("active"); 3259 nameField.required = false;
3260 emailField.required = false; 3261 } 3262 }); 3263 } 3264 } 3265 3266 function setupMediaEvents() { 3267 // Image upload 3268 const imageInput = document.getElementById("oum_location_images"); 3269 if (imageInput) { 3270 // Let OUMMedia handle the image upload 3271 OUMMedia.initializeImageUpload(imageInput); 3272 } 3273 3274 // Remove image button 3275 const removeImageBtn = document.getElementById("oum_remove_image"); 3276 if (removeImageBtn) { 3277 removeImageBtn.addEventListener("click", function() { 3278 document.getElementById("oum_location_images_preview").innerHTML = ""; 3279 }); 3280 } 3281 3282 // Remove audio button 3283 const removeAudioBtn = document.getElementById("oum_remove_audio"); 3284 if (removeAudioBtn) { 3285 removeAudioBtn.addEventListener("click", function() { 3286 const audioInput = document.getElementById("oum_location_audio"); 3287 const previewContainer = audioInput.nextElementSibling; 3288 const previewDiv = previewContainer.querySelector('.audio-preview'); 3289 3290 // Clear the file input 3291 audioInput.value = ""; 3292 3293 // Clear the preview 3294 if (previewDiv) { 3295 previewDiv.innerHTML = ''; 3296 } 3297 3298 // Remove active state 3299 previewContainer.classList.remove("active"); 3300 3301 // Set remove flag 3302 document.getElementById("oum_remove_existing_audio").value = "1"; 3303 }); 3304 } 3305 } 3306 3307 function setupCategoryDropdownIcons() { 3308 // Handle dropdown with icons for marker categories 3309 const categorySelect = document.querySelector('select#oum_marker_icon'); 3310 if (categorySelect) { 3311 // Create a custom dropdown wrapper 3312 const wrapper = document.createElement('div'); 3313 wrapper.className = 'oum-category-dropdown-wrapper'; 3314 wrapper.style.position = 'relative'; 3315 3316 // Insert wrapper before the select 3317 categorySelect.parentNode.insertBefore(wrapper, categorySelect); 3318 wrapper.appendChild(categorySelect); 3319 3320 // Create custom dropdown display 3321 const display = document.createElement('div'); 3322 display.className = 'oum-category-dropdown-display'; 3323 3324 // Create icon element 3325 const icon = document.createElement('img'); 3326 icon.className = 'oum-category-dropdown-icon'; 3327 3328 const linePreview = document.createElement('span'); 3329 linePreview.className = 'oum-category-line-preview'; 3330 3331 const areaPreview = document.createElement('span'); 3332 areaPreview.className = 'oum-category-area-preview'; 3333 3334 const routeIconTemplate = document.getElementById('oum-route-category-icon-template'); 3335 const areaIconTemplate = document.getElementById('oum-area-category-icon-template'); 3336 if (routeIconTemplate && routeIconTemplate.content) { 3337 linePreview.appendChild(routeIconTemplate.content.cloneNode(true)); 3338 } 3339 if (areaIconTemplate && areaIconTemplate.content) { 3340 areaPreview.appendChild(areaIconTemplate.content.cloneNode(true)); 3341 } 3342 3343 // Create text element 3344 const text = document.createElement('span'); 3345 text.className = 'oum-category-dropdown-text'; 3346 3347 display.appendChild(icon); 3348 display.appendChild(linePreview); 3349 display.appendChild(areaPreview); 3350 display.appendChild(text); 3351 3352 // Insert display before select 3353 wrapper.insertBefore(display, categorySelect); 3354 3355 // Make select invisible but still functional 3356 categorySelect.style.cssText = ` 3357 position: absolute; 3358 top: 0; 3359 left: 0; 3360 width: 100%; 3361 height: 100%; 3362 opacity: 0; 3363 cursor: pointer; 3364 z-index: 1; 3365 `; 3366 3367 // Update display when selection changes 3368 function updateDisplay() { 3369 const selectedOption = categorySelect.options[categorySelect.selectedIndex]; 3370 if (selectedOption && selectedOption.value) { 3371 const iconUrl = selectedOption.getAttribute('data-icon'); 3372 const categoryColor = selectedOption.getAttribute('data-color') || '#e82c71'; 3373 const categoryType = selectedOption.getAttribute('data-category-type') || 'point'; 3374 3375 if (categoryType === 'point' && iconUrl) { 3376 icon.src = iconUrl; 3377 icon.style.display = 'block'; 3378 } else { 3379 icon.style.display = 'none'; 3380 } 3381 3382 linePreview.style.color = categoryColor; 3383 linePreview.style.display = categoryType === 'polyline' ? 'inline-block' : 'none'; 3384 areaPreview.style.color = categoryColor; 3385 areaPreview.style.display = categoryType === 'polygon' ? 'inline-block' : 'none'; 3386 text.textContent = selectedOption.textContent; 3387 } else { 3388 icon.style.display = 'none'; 3389 linePreview.style.color = ''; 3390 linePreview.style.display = 'none'; 3391 areaPreview.style.color = ''; 3392 areaPreview.style.display = 'none'; 3393 text.textContent = 'Select a category...'; 3394 } 3395 } 3396 3397 // Initial display update 3398 updateDisplay(); 3399 if (typeof OUMFormMap !== "undefined" && OUMFormMap.updateLocationTypeCategoryUi) { 3400 OUMFormMap.updateLocationTypeCategoryUi(); 3401 updateDisplay(); 3402 } 3403 3404 // Handle selection change 3405 categorySelect.addEventListener('change', updateDisplay); 3406 3407 3408 // Make display focusable for accessibility 3409 display.setAttribute('tabindex', '0'); 3410 3411 // Handle keyboard navigation 3412 display.addEventListener('keydown', function(e) { 3413 if (e.key === 'Enter' || e.key === ' ') { 3414 e.preventDefault(); 3415 categorySelect.focus(); 3416 categorySelect.click(); 3417 } 3418 }); 3419 } 3420 } 3421 3422 function populateForm(location) { 3423 isEditMode = true; 3424 currentLocationId = location.post_id; 3425 3426 // Add edit-location class 3427 const addLocationEl = document.querySelector(".add-location"); 3428 if (addLocationEl) { 3429 addLocationEl.classList.add("edit-location"); 3430 } 3431 3432 // Set post_id 3433 const postIdField = document.getElementById("oum_post_id"); 3434 if (postIdField) { 3435 postIdField.value = location.post_id; 3436 } 3437 3438 // Basic fields 3439 const titleField = document.getElementById("oum_location_title"); 3440 const latField = document.getElementById("oum_location_lat"); 3441 const lngField = document.getElementById("oum_location_lng"); 3442 const addressField = document.getElementById("oum_location_address"); 3443 3444 if (titleField) titleField.value = location.title || ""; 3445 if (latField) latField.value = location.lat || ""; 3446 if (lngField) lngField.value = location.lng || ""; 3447 if (addressField) addressField.value = location.address || ""; 3448 3449 const geometryType = location.geometry_type === "polyline" || location.geometry_type === "polygon" ? location.geometry_type : "point"; 3450 OUMFormMap.setGeometryType(geometryType, { autoStartDrawing: false }); 3451 3452 // Marker types/categories 3453 if (location.types && Array.isArray(location.types)) { 3454 if (typeof oum_enable_multiple_marker_types !== 'undefined' && oum_enable_multiple_marker_types == 'true') { 3455 // Handle multiple marker types (checkboxes) 3456 const checkboxes = document.querySelectorAll('input[name="oum_marker_icon[]"]'); 3457 if (checkboxes.length > 0) {
3458 checkboxes.forEach(checkbox => { 3459 checkbox.checked = location.types.includes(checkbox.value); 3460 }); 3461 } 3462 } else { 3463 // Handle single marker type (select) 3464 const markerSelect = document.querySelector('select#oum_marker_icon'); 3465 if (markerSelect) { 3466 markerSelect.value = location.types[0] || ''; 3467 3468 // Update custom dropdown display if it exists 3469 const customDisplay = document.querySelector('.oum-category-dropdown-display'); 3470 if (customDisplay) { 3471 // Trigger the change event to update the display 3472 markerSelect.dispatchEvent(new Event('change')); 3473 } 3474 } 3475 } 3476 } 3477 3478 // Description 3479 const descriptionField = document.getElementById("oum_location_text"); 3480 if (descriptionField) { 3481 descriptionField.value = location.text || ""; 3482 } 3483 3484 // Video field 3485 const videoField = document.getElementById("oum_location_video"); 3486 if (videoField && location.video) { 3487 videoField.value = location.video; 3488 } 3489 3490 // Handle custom fields 3491 if (location.custom_fields && Array.isArray(location.custom_fields)) { 3492 location.custom_fields.forEach(field => { 3493 if (!field || typeof field.index === 'undefined') return; 3494 3495 if (field.fieldtype === 'checkbox') { 3496 const fieldValues = Array.isArray(field.val) ? field.val : [field.val]; 3497 const checkboxes = document.querySelectorAll( 3498 `input[type="checkbox"][name="oum_location_custom_fields[${field.index}][]"]` 3499 ); 3500 3501 if (checkboxes.length > 0) { 3502 checkboxes.forEach(checkbox => { 3503 if (checkbox) { 3504 const checkboxValue = checkbox.value.trim(); 3505 const normalizedValues = fieldValues.map(val => (val || '').toString().trim()); 3506 checkbox.checked = normalizedValues.includes(checkboxValue); 3507 } 3508 }); 3509 } 3510 } else if (field.fieldtype === 'radio') { 3511 const radioInputs = document.querySelectorAll( 3512 `input[type="radio"][name="oum_location_custom_fields[${field.index}]"]` 3513 ); 3514 3515 if (radioInputs.length > 0) { 3516 radioInputs.forEach(radio => { 3517 if (radio) { 3518 radio.checked = radio.value === field.val; 3519 } 3520 }); 3521 } 3522 } else if (field.fieldtype === 'select') { 3523 // Handle single and multiple select 3524 let input = document.querySelector(`[name="oum_location_custom_fields[${field.index}][]"]`); 3525 const isMultiple = !!input; 3526 if (!input) { 3527 input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`); 3528 } 3529 if (input && input.tagName === 'SELECT') { 3530 const values = Array.isArray(field.val) ? field.val.map(v => (v || '').toString().trim()) : [(field.val || '').toString().trim()]; 3531 Array.from(input.options).forEach(opt => { 3532 opt.selected = values.includes((opt.value || '').toString().trim()); 3533 }); 3534 } 3535 } else if (field.fieldtype === 'opening_hours') { 3536 // Handle opening hours field - expect JSON string 3537 try { 3538 const openingHoursData = JSON.parse(field.val); 3539 if (openingHoursData && typeof openingHoursData === 'object') { 3540 // Find the hours input 3541 const hoursInput = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`); 3542 const fieldContainer = document.querySelector(`[data-custom-field-index="${field.index}"]`); 3543 3544 // Check if 12-hour format is enabled for this field 3545 const use12hour = fieldContainer && fieldContainer.dataset.use12hour === '1'; 3546
3547 if (hoursInput && typeof OUMOpeningHours !== 'undefined') { 3548 // Convert JSON back to input format (with 12-hour format if enabled) 3549 hoursInput.value = OUMOpeningHours.formatForInput(openingHoursData, use12hour); 3550 } 3551 } 3552 } catch (e) { 3553 // Invalid JSON, try to set as raw value 3554 const input = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`); 3555 if (input) { 3556 input.value = field.val || ''; 3557 } 3558 } 3559 } else { 3560 const input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`); 3561 if (input) { 3562 input.value = field.val || ''; 3563 } 3564 } 3565 }); 3566 } 3567 3568 // Handle images 3569 if (location.image) { 3570 const imageUrls = location.image.split('|').filter(url => url); 3571 const previewContainer = document.getElementById('oum_location_images_preview'); 3572 3573 // Remove required attribute from image upload field when editing with existing images 3574 const imageInput = document.getElementById('oum_location_images'); 3575 if (imageInput && imageUrls.length > 0) { 3576 imageInput.removeAttribute('required'); 3577 } 3578 3579 if (previewContainer) { 3580 previewContainer.innerHTML = ''; 3581 3582 imageUrls.forEach(url => { 3583 if (!url) return; 3584 3585 const previewItem = OUMMedia.createExistingImagePreview(url); 3586 if (!previewItem) { 3587 return; 3588 } 3589 3590 previewItem.style.opacity = "0"; 3591 previewItem.style.transform = "scale(0.9)"; 3592 previewItem.style.transition = "all 0.3s ease"; 3593 previewItem.style.opacity = "1"; 3594 previewItem.style.transform = "scale(1)"; 3595 previewContainer.appendChild(previewItem); 3596 }); 3597 } 3598 } 3599 3600 // Handle audio 3601 if (location.audio) { 3602 OUMMedia.setExistingAudio(location.audio); 3603 document.getElementById("oum_remove_existing_audio").value = "0"; 3604 } 3605 3606 // Set map view to location 3607 if (geometryType !== "point" && location.geometry) { 3608 OUMFormMap.setPolylineGeometry(location.geometry); 3609 OUMFormMap.startEditLayersTool(); 3610 } else if (location.lat && location.lng) { 3611 const locationZoom = location.zoom ? Number(location.zoom) : 12; 3612 OUMFormMap.setView(location.lat, location.lng, locationZoom); 3613 OUMFormMap.setLocation(location.lat, location.lng); 3614 } 3615 3616 // Sync checkbox validation after form population 3617 OUMCheckboxValidation.syncAllGroups(); 3618 } 3619 3620 function resetForm() { 3621 isEditMode = false; 3622 currentLocationId = null; 3623 selectedFiles = []; 3624 window.oumSelectedFiles = []; 3625 3626 const addLocationEl = document.querySelector(".add-location"); 3627 if (addLocationEl) { 3628 addLocationEl.classList.remove("edit-location"); 3629 } 3630 3631 // Reset form and message system 3632 const form = document.getElementById("oum_add_location"); 3633 const errorDiv = document.getElementById("oum_add_location_error"); 3634 const thankyouDiv = document.getElementById("oum_add_location_thankyou"); 3635 3636 // Reset form if it exists 3637 if (form) { 3638 form.reset(); 3639 form.style.display = 'block'; 3640 } 3641 3642 if (typeof OUMFormMap !== "undefined") { 3643 const typeField = document.getElementById("oum_geometry_type"); 3644 OUMFormMap.setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false }); 3645 OUMFormMap.clearPolyline(); 3646 } 3647 3648 // Stop locate process 3649 if (window.map_locate_process) { 3650 window.map_locate_process.stop(); 3651 } 3652 3653 // Reset custom fields 3654 const customFields = document.querySelectorAll('[name^="oum_location_custom_fields"]'); 3655 if (customFields) { 3656 customFields.forEach(field => { 3657 if (field.type === 'checkbox' || field.type === 'radio') { 3658 field.checked = false; 3659 } else if (field.type === 'select-one') { 3660 field.selectedIndex = 0; 3661 } else { 3662 field.value = ''; 3663 } 3664 }); 3665 } 3666 3667 // Reset post_id field 3668 const postIdField = document.getElementById("oum_post_id"); 3669 if (postIdField) { 3670 postIdField.value = ""; 3671 } 3672 3673 // Reset oum_delete_location field 3674 const deleteLocationField = document.getElementById("oum_delete_location"); 3675 if (deleteLocationField) { 3676 deleteLocationField.value = ""; 3677 } 3678 3679 // Reset error message if it exists 3680 if (errorDiv) { 3681 errorDiv.style.display = 'none'; 3682 } 3683 3684 // Reset thank you message if it exists 3685 if (thankyouDiv) { 3686 thankyouDiv.style.display = 'none'; 3687 thankyouDiv.classList.remove('oum-delete-confirmation'); 3688 } 3689 3690 // Reset image preview 3691 const previewContainer = document.getElementById("oum_location_images_preview"); 3692 if (previewContainer) { 3693 previewContainer.innerHTML = ""; 3694 } 3695 3696 // Reset audio preview 3697 const audioInput = document.getElementById("oum_location_audio"); 3698 if (audioInput) { 3699 const previewContainer = audioInput.nextElementSibling; 3700 const previewDiv = previewContainer.querySelector('.audio-preview'); 3701 3702 // Clear the file input 3703 audioInput.value = ""; 3704 3705 // Clear the preview 3706 if (previewDiv) { 3707 previewDiv.innerHTML = ''; 3708 } 3709 3710 // Remove active state 3711 previewContainer.classList.remove("active"); 3712 } 3713 3714 // Reset hidden fields 3715 const removeExistingImage = document.getElementById("oum_remove_existing_image"); 3716 if (removeExistingImage) { 3717 removeExistingImage.value = "0"; 3718 } 3719 3720 const removeExistingAudio = document.getElementById("oum_remove_existing_audio"); 3721 if (removeExistingAudio) { 3722 removeExistingAudio.value = "0"; 3723 } 3724 3725 // Reset author section 3726 const authorSection = document.getElementById("oum_author"); 3727 if (authorSection) { 3728 authorSection.classList.remove("active"); 3729 } 3730 3731 // Reset name and email fields 3732 const nameField = document.getElementById("oum_location_author_name"); 3733 const emailField = document.getElementById("oum_location_author_email"); 3734 if (nameField && emailField) { 3735 nameField.required = false;
3736 emailField.required = false; 3737 } 3738 } 3739 3740 // Public interface 3741 return { 3742 init: function() { 3743 setupFormEvents(); 3744 setupDeleteButton(); // Add delete button handler 3745 setupCategoryDropdownIcons(); 3746 }, 3747 showFormMessage: showFormMessage, 3748 openForm: openForm, 3749 closeForm: closeForm, 3750 resetForm: resetForm, 3751 populateForm: populateForm, 3752 isEditMode: function() { 3753 return isEditMode; 3754 }, 3755 getCurrentLocationId: function() { 3756 return currentLocationId; 3757 } 3758 }; 3759})(); 3760 3761// OUMOpeningHours module is now in a separate file: frontend-opening-hours.js 3762// It's loaded as a dependency of this script 3763 3764/** 3765 * Media Module - Handles image upload and preview functionality 3766 */ 3767const OUMMedia = (function () { 3768 // Private variables 3769 let selectedFiles = []; 3770 let startX, startY, originalPosition, placeholder; 3771 let isDragging = false; 3772 3773 // Private functions 3774 function initializeImageUpload(imageInput) { 3775 if (!imageInput) return; 3776 3777 // Add click handler for upload icon label 3778 const uploadLabel = imageInput.parentElement.querySelector( 3779 'label[for="oum_location_images"]' 3780 ); 3781 if (uploadLabel) { 3782 uploadLabel.addEventListener("click", function (e) { 3783 e.preventDefault(); 3784 imageInput.click(); 3785 }); 3786 } 3787 3788 // Setup drag and drop handlers 3789 document.addEventListener("mousemove", handleDragMove); 3790 document.addEventListener("mouseup", handleDragEnd); 3791 if (imageInput.parentElement.querySelector(".image-preview-container")) { 3792 imageInput.parentElement.querySelector(".image-preview-container").addEventListener("dragover", handleDragOver); 3793 } 3794 3795 // Setup image input change handler 3796 imageInput.addEventListener("change", handleImageInputChange); 3797 } 3798 3799 function initializeAudioUpload() { 3800 const audioInput = document.getElementById('oum_location_audio'); 3801 if (!audioInput) return; 3802 3803 // Add click handler for upload icon label 3804 const uploadLabel = audioInput.parentElement.querySelector( 3805 'label[for="oum_location_audio"]' 3806 ); 3807 if (uploadLabel) { 3808 uploadLabel.addEventListener("click", function (e) { 3809 e.preventDefault(); 3810 audioInput.click(); 3811 }); 3812 } 3813 3814 // Setup audio input change handler 3815 audioInput.addEventListener("change", handleAudioInputChange); 3816 } 3817 3818 function handleAudioInputChange(e) { 3819 const audioInput = e.target; 3820 const previewContainer = audioInput.nextElementSibling; 3821 const previewDiv = previewContainer.querySelector('.audio-preview'); 3822 3823 if (audioInput.files && audioInput.files[0]) { 3824 const file = audioInput.files[0]; 3825 3826 previewContainer.classList.add('active'); 3827 3828 // Create audio preview element 3829 const audio = document.createElement('audio'); 3830 audio.controls = true; 3831 audio.style.width = '100%'; 3832 3833 const source = document.createElement('source'); 3834 source.src = URL.createObjectURL(file); 3835 source.type = file.type; 3836 3837 audio.appendChild(source); 3838 3839 // Replace existing audio preview if any 3840 previewDiv.innerHTML = ''; 3841 previewDiv.appendChild(audio); 3842 } 3843 } 3844 3845 function setExistingAudio(audioUrl) { 3846 if (!audioUrl) return; 3847 3848 const audioInput = document.getElementById('oum_location_audio'); 3849 if (!audioInput) return; 3850 3851 const previewContainer = audioInput.nextElementSibling; 3852 const previewDiv = previewContainer.querySelector('.audio-preview'); 3853 3854 previewContainer.classList.add('active'); 3855 3856 // Create audio preview element 3857 const audio = document.createElement('audio'); 3858 audio.controls = true; 3859 audio.style.width = '100%'; 3860 3861 const source = document.createElement('source'); 3862 source.src = audioUrl; 3863 source.type = 'audio/' + audioUrl.split('.').pop(); 3864 3865 audio.appendChild(source); 3866 3867 // Replace existing audio preview if any 3868 previewDiv.innerHTML = ''; 3869 previewDiv.appendChild(audio); 3870 } 3871 3872 function handleImageInputChange(e) { 3873 const imageInput = e.target; 3874 const previewContainer = document.getElementById( 3875 "oum_location_images_preview" 3876 ); 3877 const maxFiles = parseInt(imageInput.dataset.maxFiles) || 5; 3878 const maxFileSize = OUMConfig.defaults.media.maxImageSize; // in bytes 3879 3880 // Convert FileList to Array and store in a variable 3881 const files = Array.prototype.slice.call(e.target.files); 3882 const existingCount = selectedFiles.length; 3883 const totalFiles = existingCount + files.length; 3884 3885 if (totalFiles > maxFiles) { 3886 alert( 3887 OUMUtils.sprintf( 3888 oum_custom_strings.max_files_exceeded, 3889 maxFiles, 3890 maxFiles - existingCount 3891 ) 3892 ); 3893 } 3894 3895 // Process only up to remaining slots 3896 const remainingSlots = maxFiles - existingCount; 3897 const filesToProcess = files.slice(0, remainingSlots); 3898 3899 // Validate file sizes and collect valid files 3900 const validFiles = []; 3901 const invalidFiles = []; 3902 3903 filesToProcess.forEach(file => { 3904 if (file.size > maxFileSize) { 3905 invalidFiles.push(file.name); 3906 } else { 3907 validFiles.push(file); 3908 } 3909 }); 3910 3911 // Show error message for invalid files 3912 if (invalidFiles.length > 0) { 3913 const maxSizeMB = Math.round(maxFileSize / (1024 * 1024)); 3914 alert( 3915 OUMUtils.sprintf( 3916 oum_custom_strings.max_filesize_exceeded, 3917 maxSizeMB, 3918 invalidFiles.join('\n') 3919 ) 3920 ); 3921 } 3922 3923 // Update selected files with only valid ones 3924 selectedFiles = [...selectedFiles, ...validFiles]; 3925 3926 // Create previews for valid files only
3927 createImagePreviews(validFiles, previewContainer); 3928 3929 // Make selectedFiles available globally for the form submission 3930 window.oumSelectedFiles = selectedFiles; 3931 } 3932 3933 function createImagePreviews(files, container) { 3934 files.forEach((file) => { 3935 const reader = new FileReader(); 3936 3937 reader.onload = function (e) { 3938 const previewItem = createPreviewItem(e.target.result, file.name); 3939 3940 // Add the item with a fade-in animation 3941 previewItem.style.opacity = "0"; 3942 previewItem.style.transform = "scale(0.9)"; 3943 container.appendChild(previewItem); 3944 3945 // Trigger animation after a brief delay 3946 setTimeout(() => { 3947 previewItem.style.transition = "all 0.3s ease"; 3948 previewItem.style.opacity = "1"; 3949 previewItem.style.transform = "scale(1)"; 3950 }, 50); 3951 }; 3952 3953 reader.readAsDataURL(file); 3954 }); 3955 } 3956 3957 function createPreviewItem(imgSrc, fileName, isExisting) { 3958 const previewItem = document.createElement("div"); 3959 previewItem.className = isExisting ? "image-preview-item existing-image" : "image-preview-item"; 3960 if (fileName) { 3961 previewItem.dataset.fileName = fileName; 3962 } 3963 if (imgSrc) { 3964 previewItem.dataset.url = imgSrc; 3965 } 3966 3967 const img = document.createElement("img"); 3968 img.setAttribute("alt", "Preview"); 3969 img.src = imgSrc; 3970 3971 const remove = document.createElement("div"); 3972 remove.className = "remove-image"; 3973 remove.setAttribute("title", "Remove image"); 3974 remove.textContent = "\u00d7"; 3975 3976 const handle = document.createElement("div"); 3977 handle.className = "drag-handle"; 3978 handle.setAttribute("title", "Drag to reorder"); 3979 handle.textContent = "\u22ee\u22ee"; 3980 3981 previewItem.appendChild(img); 3982 previewItem.appendChild(remove); 3983 previewItem.appendChild(handle); 3984 3985 if (isExisting) { 3986 const hidden = document.createElement("input"); 3987 hidden.type = "hidden"; 3988 hidden.name = "existing_images[]"; 3989 hidden.value = imgSrc; 3990 previewItem.appendChild(hidden); 3991 } 3992 3993 // Add event listener for remove button 3994 const removeButton = previewItem.querySelector('.remove-image'); 3995 if (removeButton) { 3996 removeButton.addEventListener('click', handleRemoveImage); 3997 } 3998 3999 // Set up drag and drop for existing images 4000 setupDragAndDrop(previewItem); 4001 4002 return previewItem; 4003 } 4004 4005 function handleRemoveImage(e) { 4006 e.preventDefault(); 4007 const previewItem = this.closest(".image-preview-item"); 4008 4009 // If it's an existing image, handle removal differently 4010 if (previewItem.classList.contains("existing-image")) { 4011 const imgUrl = previewItem.querySelector("[name='existing_images[]']").value; 4012 const removedImagesInput = document.getElementById("oum_remove_existing_image"); 4013 const currentValue = removedImagesInput.value === "0" ? [] : removedImagesInput.value.split('|'); 4014 currentValue.push(imgUrl); 4015 removedImagesInput.value = currentValue.join('|'); 4016 } else { 4017 // Remove from selectedFiles array if it's a new image 4018 const fileName = previewItem.dataset.fileName; 4019 selectedFiles = selectedFiles.filter(file => file.name !== fileName); 4020 window.oumSelectedFiles = selectedFiles; 4021 } 4022 4023 // Animate and remove the preview item 4024 previewItem.style.transition = "all 0.3s ease"; 4025 previewItem.style.transform = "scale(0.8)"; 4026 previewItem.style.opacity = "0"; 4027 4028 setTimeout(() => { 4029 previewItem.remove(); 4030 }, 300); 4031 } 4032 4033 function setupDragAndDrop(previewItem) { 4034 previewItem.setAttribute('draggable', 'true'); 4035 4036 previewItem.addEventListener('mousedown', function(e) { 4037 if (e.target.classList.contains('remove-image')) return; 4038 4039 isDragging = true; 4040 this.classList.add('dragging'); 4041 4042 // Get element dimensions once at start 4043 const rect = this.getBoundingClientRect(); 4044 this.style.width = rect.width + 'px'; 4045 this.style.height = rect.height + 'px'; 4046 4047 // Store initial grid container for safety check 4048 this.initialContainer = this.closest('.oum-image-preview-grid'); 4049 4050 // Create placeholder immediately 4051 createPlaceholder(this); 4052 4053 // Set up dragged element 4054 this.style.position = 'fixed'; 4055 this.style.zIndex = '1000'; 4056 this.style.opacity = '0.9'; 4057 this.style.transform = 'scale(1.05) rotate(1deg)'; 4058 this.style.pointerEvents = 'none'; 4059 this.style.boxShadow = '0 5px 15px rgba(0,0,0,0.15)'; 4060 4061 // Set initial position 4062 moveDraggedElement(this, e); 4063 4064 document.body.style.cursor = 'grabbing'; 4065 }); 4066 4067 previewItem.addEventListener('touchstart', handleTouchStart); 4068 } 4069 4070 function createPlaceholder(element) { 4071 placeholder = document.createElement("div"); 4072 placeholder.classList.add("image-preview-placeholder"); 4073 placeholder.style.width = element.offsetWidth + "px"; 4074 placeholder.style.height = element.offsetHeight + "px"; 4075 placeholder.style.transition = "transform 0.2s ease"; 4076 placeholder.style.border = "2px dashed #e82c71"; 4077 placeholder.style.borderRadius = "4px"; 4078 placeholder.style.backgroundColor = "rgba(224, 42, 175, 0.05)"; 4079 element.parentNode.insertBefore(placeholder, element); 4080 } 4081 4082 function moveDraggedElement(draggable, e) { 4083 const rect = draggable.getBoundingClientRect(); 4084 const centerOffsetX = rect.width / 2; 4085 const centerOffsetY = rect.height / 2; 4086 4087 // Position element directly at cursor with center offset 4088 draggable.style.left = (e.clientX - centerOffsetX) + 'px'; 4089 draggable.style.top = (e.clientY - centerOffsetY) + 'px'; 4090 } 4091 4092 function updatePlaceholderPosition(e) { 4093 const previewContainer = document.getElementById("oum_location_images_preview"); 4094 if (!previewContainer) return; 4095 4096 const draggable = document.querySelector('.dragging'); 4097 if (!draggable) return; 4098 4099 const siblings = [...previewContainer.querySelectorAll(".image-preview-item:not(.dragging)")]; 4100 4101 // Find the closest sibling based on mouse position 4102 const closestSibling = siblings.reduce((closest, child) => { 4103 const rect = child.getBoundingClientRect(); 4104 const centerX = rect.left + rect.width / 2; 4105 const offset = e.clientX - centerX; 4106
4107 if (offset < 0 && (!closest.element || offset > closest.offset)) { 4108 return { offset: offset, element: child }; 4109 } 4110 return closest; 4111 }, { offset: Number.NEGATIVE_INFINITY, element: null }); 4112 4113 if (closestSibling.element) { 4114 previewContainer.insertBefore(placeholder, closestSibling.element); 4115 } else { 4116 previewContainer.appendChild(placeholder); 4117 } 4118 } 4119 4120 function handleDragMove(e) { 4121 if (!isDragging) return; 4122 4123 const draggable = document.querySelector(".dragging"); 4124 if (!draggable) return; 4125 4126 // Update dragged element position 4127 moveDraggedElement(draggable, e); 4128 4129 // Check if cursor is still within any grid container 4130 const gridContainer = document.getElementById("oum_location_images_preview"); 4131 if (!gridContainer) return; 4132 4133 const gridRect = gridContainer.getBoundingClientRect(); 4134 const isWithinGrid = e.clientX >= gridRect.left - 50 && 4135 e.clientX <= gridRect.right + 50 && 4136 e.clientY >= gridRect.top - 50 && 4137 e.clientY <= gridRect.bottom + 50; 4138 4139 // If cursor is outside grid boundaries, hide placeholder 4140 if (!isWithinGrid && placeholder) { 4141 placeholder.style.display = 'none'; 4142 } else if (placeholder) { 4143 placeholder.style.display = 'block'; 4144 updatePlaceholderPosition(e); 4145 } 4146 } 4147 4148 function handleDragEnd() { 4149 const draggable = document.querySelector(".dragging"); 4150 if (!draggable) return; 4151 4152 // Reset cursor 4153 document.body.style.cursor = ""; 4154 4155 // Check if we're still within the grid 4156 const gridContainer = document.getElementById("oum_location_images_preview"); 4157 if (!gridContainer) { 4158 // If no grid found, return item to its initial position 4159 if (draggable.initialContainer) { 4160 draggable.initialContainer.appendChild(draggable); 4161 } 4162 } else { 4163 // Place draggable element at placeholder position if within grid 4164 if (placeholder && placeholder.style.display !== 'none') { 4165 draggable.style.transition = "none"; 4166 placeholder.parentNode.insertBefore(draggable, placeholder); 4167 } else { 4168 // If placeholder is hidden (outside grid), append to end 4169 gridContainer.appendChild(draggable); 4170 } 4171 } 4172 4173 // Remove placeholder 4174 if (placeholder) { 4175 placeholder.remove(); 4176 } 4177 4178 // Reset draggable element styles 4179 draggable.style.position = ""; 4180 draggable.style.zIndex = ""; 4181 draggable.style.top = ""; 4182 draggable.style.left = ""; 4183 draggable.style.width = ""; 4184 draggable.style.height = ""; 4185 draggable.style.transform = ""; 4186 draggable.style.pointerEvents = ""; 4187 draggable.style.boxShadow = ""; 4188 draggable.classList.remove("dragging"); 4189 4190 isDragging = false; 4191 } 4192 4193 function handleTouchStart(e) { 4194 const touch = e.touches[0]; 4195 const mouseEvent = new MouseEvent("mousedown", { 4196 clientX: touch.clientX, 4197 clientY: touch.clientY 4198 }); 4199 this.dispatchEvent(mouseEvent); 4200 } 4201 4202 function handleDragOver(e) { 4203 e.preventDefault(); 4204 e.stopPropagation(); 4205 4206 const draggable = document.querySelector(".dragging"); 4207 if (!draggable) return; 4208 4209 const afterElement = getDragAfterElement(this, e.clientX); 4210 if (afterElement == null) { 4211 this.appendChild(placeholder); 4212 } else { 4213 this.insertBefore(placeholder, afterElement); 4214 } 4215 } 4216 4217 // Public interface 4218 return { 4219 init: function () { 4220 initializeAudioUpload(); 4221 }, 4222 initializeImageUpload: function(imageInput) { 4223 initializeImageUpload(imageInput); 4224 }, 4225 getSelectedFiles: function () { 4226 return selectedFiles; 4227 }, 4228 setupDragAndDrop: setupDragAndDrop, 4229 handleRemoveImage: handleRemoveImage, 4230 createExistingImagePreview: function(url) { 4231 if (!url) { 4232 return null; 4233 } 4234 return createPreviewItem(url, '', true); 4235 }, 4236 setExistingAudio: setExistingAudio 4237 }; 4238})(); 4239 4240/** 4241 * Checkbox Validation Module - Handles required checkbox group validation 4242 */ 4243const OUMCheckboxValidation = (function () { 4244 // Private functions 4245 function syncRequired(group) { 4246 const anyChecked = group.some(cb => cb.checked); 4247 const first = group[0]; 4248 first.required = !anyChecked; 4249 } 4250 4251 function getCheckboxGroups() { 4252 const checkboxGroups = document.querySelectorAll('.oum-checkbox-group.is-required input[type="checkbox"]'); 4253 4254 if (!checkboxGroups.length) return {}; 4255 4256 // Group checkboxes by name 4257 const groups = {}; 4258 checkboxGroups.forEach(checkbox => { 4259 const name = checkbox.name; 4260 if (!groups[name]) groups[name] = []; 4261 groups[name].push(checkbox); 4262 }); 4263 4264 return groups; 4265 } 4266 4267 function processCheckboxGroup(group, setupEvents = true) { 4268 const first = group[0]; 4269 4270 // Only process if first checkbox is required 4271 if (!first.required) return; 4272 4273 // Add event listeners if requested 4274 if (setupEvents) { 4275 group.forEach(cb => cb.addEventListener('change', () => syncRequired(group))); 4276 } 4277 4278 // Sync the state 4279 syncRequired(group); 4280 } 4281 4282 function initCheckboxGroups() { 4283 const groups = getCheckboxGroups(); 4284 4285 // Process each group with event listeners
4286 Object.keys(groups).forEach(groupName => { 4287 processCheckboxGroup(groups[groupName], true); 4288 }); 4289 } 4290 4291 // Public interface 4292 return { 4293 init: function() { 4294 initCheckboxGroups(); 4295 }, 4296 syncAllGroups: function() { 4297 const groups = getCheckboxGroups(); 4298 4299 // Sync each group without setting up new event listeners 4300 Object.keys(groups).forEach(groupName => { 4301 processCheckboxGroup(groups[groupName], false); 4302 }); 4303 } 4304 }; 4305})(); 4306 4307/** 4308 * Advanced Filter Interface Module - Handles advanced filtering functionality 4309 */ 4310const OUMAdvancedFilter = (function () { 4311 // Private variables 4312 let isInitialized = false; 4313 let filterSidebar = null; 4314 4315 // Private functions 4316 function init() { 4317 if (isInitialized) return; 4318 4319 filterSidebar = document.querySelector('.oum-advanced-filter-sidebar'); 4320 if (!filterSidebar) { 4321 return; 4322 } 4323 4324 setupFilterEvents(); 4325 setupFloatingPanelToggle(); 4326 isInitialized = true; 4327 } 4328 4329 // Setup click toggle for button/panel 4330 function setupFloatingPanelToggle() { 4331 const floatingPanel = document.querySelector('.oum-advanced-filter-button, .oum-advanced-filter-panel'); 4332 if (!floatingPanel || !floatingPanel.classList.contains('use-collapse')) { 4333 return; 4334 } 4335 4336 const toggle = floatingPanel.querySelector('.oum-advanced-filter-toggle'); 4337 const closeButton = floatingPanel.querySelector('.close-advanced-filter'); 4338 const filterWrapper = floatingPanel.closest('.oum-map-filter-wrapper'); 4339 4340 // Toggle active class on click 4341 if (toggle) { 4342 toggle.addEventListener('click', function(e) { 4343 e.stopPropagation(); 4344 floatingPanel.classList.toggle('active'); 4345 }); 4346 } 4347 4348 // Close button always collapses the floating panel 4349 if (closeButton) { 4350 closeButton.addEventListener('click', function(e) { 4351 e.stopPropagation(); 4352 floatingPanel.classList.remove('active'); 4353 }); 4354 } 4355 4356 // Close when clicking outside the filter wrapper 4357 if (filterWrapper) { 4358 document.addEventListener('click', function(e) { 4359 if (!filterWrapper.contains(e.target)) { 4360 floatingPanel.classList.remove('active'); 4361 } 4362 }); 4363 } 4364 } 4365 4366 function setupFilterEvents() { 4367 // Get all filter inputs 4368 const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input'); 4369 const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox'); 4370 const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio'); 4371 const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select'); 4372 4373 // Add event listeners to all filter inputs 4374 filterInputs.forEach(input => { 4375 input.addEventListener('input', handleFilterChange); 4376 }); 4377 4378 filterCheckboxes.forEach(checkbox => { 4379 checkbox.addEventListener('change', handleFilterChange); 4380 }); 4381 4382 filterRadios.forEach(radio => { 4383 radio.addEventListener('change', handleFilterChange); 4384 }); 4385 4386 filterSelects.forEach(select => { 4387 select.addEventListener('change', handleFilterChange); 4388 }); 4389 4390 // Add reset button functionality 4391 const resetButton = filterSidebar.querySelector('#oum-advanced-filter-reset'); 4392 if (resetButton) { 4393 resetButton.addEventListener('click', resetAllFilters); 4394 } 4395 } 4396 4397 function handleFilterChange() { 4398 if (typeof OUMMarkers === 'undefined' || !OUMMarkers.setCustomfieldsFilter) { 4399 console.warn('OUMMarkers not available for advanced filtering'); 4400 return; 4401 } 4402 4403 const filterCriteria = getFilterCriteria(); 4404 OUMMarkers.setCustomfieldsFilter(filterCriteria); 4405 } 4406 4407 function getFilterCriteria() { 4408 const criteria = {}; 4409 if (!filterSidebar) return criteria; 4410 4411 // Get text/email/url inputs 4412 const textInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input'); 4413 textInputs.forEach(input => { 4414 const customFieldId = input.dataset.customFieldId; 4415 if (customFieldId && input.value.trim()) { 4416 criteria[customFieldId] = { 4417 type: 'text', 4418 value: input.value.trim().toLowerCase() 4419 }; 4420 } 4421 }); 4422 4423 // Get checkbox selections 4424 const checkboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4425 checkboxes.forEach(checkbox => { 4426 const customFieldId = checkbox.dataset.customFieldId; 4427 const filterType = checkbox.dataset.filterType; 4428 4429 if (customFieldId) { 4430 // Check if this is an opening_hours open_now filter 4431 if (filterType === 'opening_hours_open_now') { 4432 if (checkbox.checked) { 4433 criteria[customFieldId] = { 4434 type: 'opening_hours_open_now', 4435 value: 'open' 4436 }; 4437 } 4438 } else { 4439 // Standard checkbox handling 4440 if (!criteria[customFieldId]) { 4441 // Get checkbox relation from the fieldset (default to OR for backward compatibility) 4442 const fieldset = checkbox.closest('.oum-checkbox-group'); 4443 const relation = fieldset ? (fieldset.dataset.checkboxRelation || 'OR') : 'OR'; 4444 criteria[customFieldId] = { type: 'checkbox', values: [], relation: relation }; 4445 } 4446 if (checkbox.checked) { 4447 criteria[customFieldId].values.push(checkbox.value); 4448 } 4449 } 4450 } 4451 }); 4452 4453 // Get radio selections 4454 const radios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio'); 4455 radios.forEach(radio => { 4456 const customFieldId = radio.dataset.customFieldId; 4457 if (customFieldId && radio.checked) { 4458 criteria[customFieldId] = { 4459 type: 'radio', 4460 value: radio.value 4461 }; 4462 } 4463 }); 4464 4465 // Get select selections 4466 const selects = filterSidebar.querySelectorAll('.oum-advanced-filter-select'); 4467 selects.forEach(select => { 4468 const customFieldId = select.dataset.customFieldId; 4469 if (customFieldId && select.value) { 4470 criteria[customFieldId] = { 4471 type: 'select', 4472 value: select.value 4473 }; 4474 } 4475 }); 4476 4477 // Remove empty criteria (no checkboxes selected, empty text inputs, etc.) 4478 Object.keys(criteria).forEach(key => { 4479 const criterion = criteria[key]; 4480 if (criterion.type === 'checkbox' && criterion.values.length === 0) { 4481 delete criteria[key]; 4482 } 4483 }); 4484 4485 return criteria; 4486 } 4487 4488 function resetAllFilters() { 4489 // Clear all text inputs 4490 const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input'); 4491 filterInputs.forEach(input => { 4492 input.value = ''; 4493 }); 4494 4495 // Uncheck all checkboxes 4496 const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox'); 4497 filterCheckboxes.forEach(checkbox => { 4498 checkbox.checked = false; 4499 }); 4500 4501 // Uncheck all radio buttons 4502 const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio'); 4503 filterRadios.forEach(radio => { 4504 radio.checked = false; 4505 }); 4506 4507 // Reset all select dropdowns to first option 4508 const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select'); 4509 filterSelects.forEach(select => { 4510 select.selectedIndex = 0; 4511 }); 4512 4513 if (typeof OUMMarkers !== 'undefined' && OUMMarkers.setCustomfieldsFilter) { 4514 OUMMarkers.setCustomfieldsFilter(null); 4515 } 4516 4517 // Restore Marker Categories to the default "all checked" state 4518 if (typeof OUMMarkers !== 'undefined' && OUMMarkers.resetCategoriesFilter) { 4519 OUMMarkers.resetCategoriesFilter(); 4520 } 4521 4522 resetSearchbar(); 4523 } 4524 4525 function resetSearchbar() { 4526 // Live Filter Markers 4527 const liveFilterInput = document.getElementById("oum_filter_markers"); 4528 if (liveFilterInput) { 4529 liveFilterInput.value = ""; 4530 if (typeof OUMMarkers !== "undefined" && OUMMarkers.updateSearchtextAndCategoriesFilters) { 4531 OUMMarkers.updateSearchtextAndCategoriesFilters(); 4532 } 4533 } 4534 4535 // Marker search (leaflet-search) 4536 const markerSearchCancel = document.querySelector("#oum_search_marker .search-cancel"); 4537 if (markerSearchCancel) { 4538 markerSearchCancel.click(); 4539 } else { 4540 const markerSearchInput = document.querySelector("#oum_search_marker .search-input"); 4541 if (markerSearchInput) { 4542 markerSearchInput.value = ""; 4543 markerSearchInput.dispatchEvent(new Event("input", { bubbles: true })); 4544 markerSearchInput.dispatchEvent(new Event("keyup", { bubbles: true })); 4545 } 4546 } 4547 4548 // Address search (GeoSearch) 4549 const addressReset = document.querySelector("#oum_search_address button.reset, .oum-searchbar-filter .leaflet-geosearch-bar button.reset"); 4550 if (addressReset) { 4551 addressReset.click(); 4552 } else { 4553 const addressInput = document.querySelector("#oum_search_address input, .oum-searchbar-filter .leaflet-geosearch-bar input"); 4554 if (addressInput) { 4555 addressInput.value = ""; 4556 } 4557 } 4558 } 4559 4560 // Public interface 4561 return { 4562 init: init 4563 }; 4564})(); 4565 4566// Main initialization function 4567function oumInitializeMap() { 4568 // Only proceed if we have a map element 4569 if (!document.getElementById(map_el)) { 4570 console.warn('â¼ï¸ Open User Map: Map container missing. Initialization aborted. Disable page caching if applicable, or contact [email protected] for help.'); 4571 return; 4572 } 4573 4574 // Initialize opening hours UI 4575 OUMOpeningHours.init(); 4576 4577 // Restore the extended L object 4578 window.L = window.OUMLeaflet.L; 4579 4580 // Initialize map and get instance 4581 const mapInstance = OUMMap.init(map_el); 4582 4583 // Initialize markers 4584 const markersModule = OUMMarkers.init(mapInstance); 4585 4586 // Add markers from the global oum_all_locations 4587 if ( 4588 typeof oum_all_locations !== "undefined" && 4589 Array.isArray(oum_all_locations) 4590 ) { 4591 markersModule.addMarkers(oum_all_locations); 4592 } 4593 4594 // Initialize location form 4595 OUMFormMap.init(mapInstance); 4596 4597 // Initialize form controller 4598 OUMFormController.init(); 4599 4600 // Initialize media handling 4601 OUMMedia.init(); 4602 4603 // Initialize checkbox validation 4604 OUMCheckboxValidation.init(); 4605 4606 // Initialize Advanced Filter Interface 4607 OUMAdvancedFilter.init(); 4608 4609 // Setup filter events 4610 const markerFilterInput = document.getElementById("oum_filter_markers"); 4611 if (markerFilterInput) { 4612 markerFilterInput.addEventListener("input", OUMMarkers.updateSearchtextAndCategoriesFilters); 4613 } 4614 4615 const categoryInputs = document.querySelectorAll(
4616 '.open-user-map .oum-marker-categories-filter [name="type"]' 4617 ); 4618 if (categoryInputs.length > 0) { 4619 categoryInputs.forEach((input) => { 4620 input.addEventListener("change", OUMMarkers.updateSearchtextAndCategoriesFilters); 4621 }); 4622 } 4623 4624 // Execute custom JS from settings 4625 if (typeof custom_js !== "undefined" && custom_js.snippet) { 4626 try { 4627 // Wrap custom JS execution in a try-catch with proper element existence checks 4628 const wrappedJS = ` 4629 try { 4630 if (typeof document !== 'undefined') { 4631 // Defer map2-related code execution 4632 if (${custom_js.snippet.includes("oumMap2")}) { 4633 // Create a MutationObserver to watch for the form map initialization 4634 const observer = new MutationObserver((mutations) => { 4635 if (window.oumMap2) { 4636 // Execute the custom JS only when oumMap2 is available 4637 try { 4638 ${custom_js.snippet} 4639 } catch (e) { 4640 console.warn('Custom JS execution error (deferred):', e); 4641 } 4642 observer.disconnect(); 4643 } 4644 }); 4645 4646 // Start observing the document for the form map to be added 4647 observer.observe(document.body, { 4648 childList: true, 4649 subtree: true 4650 }); 4651 } else { 4652 // Execute non-map2 related code immediately 4653 ${custom_js.snippet} 4654 } 4655 } 4656 } catch (e) { 4657 console.warn('Custom JS execution error:', e); 4658 } 4659 `; 4660 Function(wrappedJS)(); 4661 } catch (error) { 4662 console.warn("Error executing custom JS:", error); 4663 } 4664 } 4665}
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.