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 /** 1300 * GestureHandling shows "use two fingers to move the map" on one-finger touchmove. 1301 * Leaflet already stops touchstart on popups, but nested popup content still 1302 * bubbles touchmove to the map container. Stop that so scrolling the bubble 1303 * does not trigger the overlay. 1304 */ 1305 function preventGestureHandlingInsidePopup(popup) { 1306 const popupEl = popup && typeof popup.getElement === "function" ? popup.getElement() : null; 1307 if (!popupEl || popupEl.dataset.oumIgnoreGestures === "true") { 1308 return; 1309 } 1310 1311 popupEl.dataset.oumIgnoreGestures = "true"; 1312 L.DomEvent.on(popupEl, "touchmove", L.DomEvent.stopPropagation); 1313 } 1314 1315 function setupMarkerEvents() { 1316 map.on("popupopen", function (locationBubble) { 1317 const popup = locationBubble.popup; 1318 preventGestureHandlingInsidePopup(popup); 1319 const marker = popup._source; 1320 1321 const applyBubbleHtml = (html) => { 1322 popup.setContent(html); 1323 if (typeof popup.update === "function") { 1324 popup.update(); 1325 } 1326 finalizeMarkerBubbleUI(popup); 1327 }; 1328 1329 if (!marker || !marker.options || !marker.options.post_id) { 1330 finalizeMarkerBubbleUI(popup); 1331 return; 1332 } 1333 1334 if (marker._oumBubbleHtmlCache) { 1335 applyBubbleHtml(marker._oumBubbleHtmlCache); 1336 return; 1337 } 1338 1339 marker._oumBubbleFetchId = (marker._oumBubbleFetchId || 0) + 1; 1340 const myFetchId = marker._oumBubbleFetchId; 1341 const postId = marker.options.post_id; 1342 1343 popup.setContent(oumBubbleLoadingHtml()); 1344 if (typeof popup.update === "function") { 1345 popup.update(); 1346 } 1347 finalizeMarkerBubbleUI(popup); 1348 1349 if ( 1350 typeof jQuery === "undefined" || 1351 !oum_ajax || 1352 !oum_ajax.ajaxurl 1353 ) { 1354 applyBubbleHtml(oumBubbleErrorHtml()); 1355 return; 1356 } 1357 1358 jQuery.ajax({ 1359 type: "POST", 1360 url: oum_ajax.ajaxurl, 1361 dataType: "json", 1362 data: { 1363 action: oum_ajax.bubble_action || "oum_get_location_bubble", 1364 post_id: postId, 1365 }, 1366 success: function (response) { 1367 if (myFetchId !== marker._oumBubbleFetchId) { 1368 return; 1369 } 1370 if (response && response.success && response.data && response.data.html) { 1371 marker._oumBubbleHtmlCache = response.data.html; 1372 applyBubbleHtml(response.data.html); 1373 } else { 1374 applyBubbleHtml(oumBubbleErrorHtml()); 1375 } 1376 }, 1377 error: function () { 1378 if (myFetchId !== marker._oumBubbleFetchId) { 1379 return; 1380 } 1381 applyBubbleHtml(oumBubbleErrorHtml()); 1382 }, 1383 }); 1384 }); 1385 1386 // Event: Close Location Bubble 1387 map.on("popupclose", function () { 1388 const el = document.querySelector(
1389 ".open-user-map #location-fullscreen-container" 1390 ); 1391 if (el) { 1392 // Clear the content to stop any media playback (YouTube, Vimeo, etc.) 1393 const locationContentWrap = el.querySelector(".location-content-wrap"); 1394 if (locationContentWrap) { 1395 locationContentWrap.innerHTML = ''; 1396 } 1397 el.classList.remove("visible"); 1398 } 1399 document.querySelector("body").classList.remove("oum-location-opened"); 1400 }); 1401 } 1402 1403 /** 1404 * Check edit permission for a location and inject edit button if allowed 1405 * This is called when a popup opens to dynamically show edit buttons 1406 * based on real-time permissions (cache-safe solution) 1407 * 1408 * @param {HTMLElement} container - The container element to search for placeholder 1409 */ 1410 function checkEditPermissionAndInjectButton(container) { 1411 // Find the edit button placeholder in the opened popup 1412 const placeholder = container.querySelector('.edit-location-button-placeholder'); 1413 1414 if (!placeholder) { 1415 return; // No placeholder found, nothing to do 1416 } 1417 1418 const postId = placeholder.getAttribute('data-post-id'); 1419 1420 if (!postId) { 1421 placeholder.remove(); 1422 return; 1423 } 1424 1425 if (typeof jQuery === 'undefined') { 1426 placeholder.remove(); 1427 return; 1428 } 1429 1430 if (!oum_ajax || !oum_ajax.ajaxurl) { 1431 placeholder.remove(); 1432 return; 1433 } 1434 1435 // Make AJAX call to check permissions 1436 jQuery.ajax({ 1437 type: 'POST', 1438 url: oum_ajax.ajaxurl, 1439 dataType: 'json', 1440 data: { 1441 action: 'oum_check_edit_permission', 1442 post_id: postId 1443 }, 1444 success: function(response) { 1445 if (response && response.success && response.data && response.data.can_edit) { 1446 // User can edit - inject the edit button 1447 const editButton = document.createElement('div'); 1448 editButton.className = 'edit-location-button'; 1449 editButton.setAttribute('data-post-id', postId); 1450 editButton.setAttribute('title', oum_custom_strings && oum_custom_strings.edit_location ? oum_custom_strings.edit_location : 'Edit location'); 1451 1452 // Replace placeholder with actual button 1453 placeholder.parentNode.replaceChild(editButton, placeholder); 1454 } else { 1455 // User cannot edit - remove placeholder (keep clean HTML) 1456 placeholder.remove(); 1457 } 1458 }, 1459 error: function(xhr, status, error) { 1460 // On error, just remove the placeholder 1461 placeholder.remove(); 1462 } 1463 }); 1464 } 1465 1466 function applyFilters() { 1467 if (!markersLayer) { 1468 return; 1469 } 1470 1471 markersLayer.clearLayers(); 1472 if (vectorLayer) { 1473 vectorLayer.clearLayers(); 1474 } 1475 const filteredMarkers = []; 1476 1477 allMarkers.forEach((marker) => { 1478 if ( 1479 markerMatchesSearchtextFilter(marker) && 1480 markerMatchesCategoriesFilter(marker) && 1481 markerMatchesCustomfieldsFilter(marker) 1482 ) { 1483 filteredMarkers.push(marker); 1484 } 1485 }); 1486 1487 visibleMarkersCount = filteredMarkers.length; 1488 addMarkersToLayer(filteredMarkers); 1489 1490 if (oum_enable_cluster && typeof markersLayer.refreshClusters === "function") { 1491 markersLayer.refreshClusters(); 1492 } 1493 } 1494 1495 function addMarkersToLayer(markers) { 1496 if (!markersLayer || !markers.length) { 1497 return; 1498 } 1499 1500 const pointMarkers = []; 1501 const vectorMarkers = []; 1502 1503 markers.forEach((marker) => { 1504 if (marker instanceof L.Marker) { 1505 pointMarkers.push(marker); 1506 } else { 1507 vectorMarkers.push(marker); 1508 } 1509 }); 1510 1511 vectorMarkers.forEach((marker) => { 1512 if (vectorLayer) { 1513 vectorLayer.addLayer(marker); 1514 } 1515 }); 1516 1517 if (!pointMarkers.length) { 1518 return; 1519 } 1520 1521 // Batch addLayers() sometimes draws no marker when only one exists; one addLayer() avoids that bug. 1522 if ( 1523 oum_enable_cluster && 1524 pointMarkers.length === 1 && 1525 typeof markersLayer.addLayer === "function" 1526 ) { 1527 markersLayer.addLayer(pointMarkers[0]); 1528 return; 1529 } 1530 1531 // Batch marker insertion when supported to improve large dataset performance. 1532 if (typeof markersLayer.addLayers === "function") { 1533 markersLayer.addLayers(pointMarkers); 1534 return; 1535 } 1536
1537 pointMarkers.forEach((marker) => { 1538 markersLayer.addLayer(marker); 1539 }); 1540 } 1541 1542 function markerMatchesSearchtextFilter(marker) { 1543 if (!searchtextFilterValue) { 1544 return true; 1545 } 1546 return marker.options.content.toLowerCase().includes(searchtextFilterValue); 1547 } 1548 1549 function markerMatchesCategoriesFilter(marker) { 1550 const markerTypes = marker.options.types || []; 1551 1552 if (!markerTypes.length) { 1553 return true; 1554 } 1555 1556 // Before the user interacts with the category checkboxes, keep the previous behaviour (show all) 1557 if (categoriesFilterSelection === null) { 1558 return true; 1559 } 1560 1561 if (!categoriesFilterSelection.size) { 1562 return false; 1563 } 1564 1565 return markerTypes.some((type) => categoriesFilterSelection.has(type)); 1566 } 1567 1568 function markerMatchesCustomfieldsFilter(marker) { 1569 if (!customfieldsFilterSelection || !Object.keys(customfieldsFilterSelection).length) { 1570 return true; 1571 } 1572 1573 const locationData = getLocationData(marker.options.post_id); 1574 if (!locationData) { 1575 return true; 1576 } 1577 1578 for (const [customFieldId, criterion] of Object.entries(customfieldsFilterSelection)) { 1579 // Special handling for opening_hours_open_now filter type 1580 if (criterion.type === 'opening_hours_open_now') { 1581 const field = locationData.custom_fields?.find( 1582 (customField) => String(customField.index) === String(customFieldId) 1583 ); 1584 1585 // Use pre-calculated open_now value from PHP 1586 if (field && typeof field.open_now === 'boolean') { 1587 if (field.open_now !== true) { 1588 return false; 1589 } 1590 } else { 1591 // Fallback: if open_now not available, exclude location 1592 return false; 1593 } 1594 continue; 1595 } 1596 1597 // Standard custom field filtering 1598 const fieldValue = getCustomFieldValue(locationData, customFieldId); 1599 if (!matchesCriterion(fieldValue, criterion)) { 1600 return false; 1601 } 1602 } 1603 1604 return true; 1605 } 1606 1607 function getLocationData(postId) { 1608 if (!postId) { 1609 return null; 1610 } 1611 1612 if (locationsById[String(postId)]) { 1613 return locationsById[String(postId)]; 1614 } 1615 1616 if (typeof window !== "undefined" && Array.isArray(window.oum_all_locations)) { 1617 const fallback = window.oum_all_locations.find( 1618 (location) => String(location.post_id) === String(postId) 1619 ); 1620 if (fallback) { 1621 locationsById[String(postId)] = fallback; 1622 return fallback; 1623 } 1624 } 1625 1626 return null; 1627 } 1628 1629 function getCustomFieldValue(locationData, customFieldId) { 1630 if (!locationData || !Array.isArray(locationData.custom_fields)) { 1631 return null; 1632 } 1633 1634 const field = locationData.custom_fields.find( 1635 (customField) => String(customField.index) === String(customFieldId) 1636 ); 1637 1638 return field ? field.val : null; 1639 } 1640 1641 function normalizeFieldValues(fieldValue) { 1642 if (Array.isArray(fieldValue)) { 1643 return fieldValue.map((val) => val.toString()).filter((val) => val !== ""); 1644 } 1645 1646 if (typeof fieldValue === "string") { 1647 return fieldValue 1648 .split("|") 1649 .map((val) => val.trim()) 1650 .filter((val) => val !== ""); 1651 } 1652 1653 if (fieldValue !== null && typeof fieldValue !== "undefined") { 1654 const value = fieldValue.toString(); 1655 return value ? [value] : []; 1656 } 1657 1658 return []; 1659 } 1660 1661 function matchesCriterion(fieldValue, criterion) { 1662 if ( 1663 fieldValue === null || 1664 typeof fieldValue === "undefined" || 1665 (typeof fieldValue === "string" && fieldValue.trim() === "") || 1666 (Array.isArray(fieldValue) && fieldValue.length === 0) 1667 ) { 1668 return false; 1669 } 1670 1671 switch (criterion.type) { 1672 case "text": 1673 return fieldValue.toString().toLowerCase().includes(criterion.value); 1674 case "checkbox": { 1675 const values = normalizeFieldValues(fieldValue); 1676 const relation = criterion.relation || 'OR'; // Default to OR for backward compatibility 1677 1678 if (relation === 'AND') { 1679 // AND: All selected checkbox values must be present in the field values 1680 return criterion.values.length > 0 && criterion.values.every((val) => values.includes(val)); 1681 } else { 1682 // OR: At least one selected checkbox value must match (default behavior)
1683 return criterion.values.some((val) => values.includes(val)); 1684 } 1685 } 1686 case "radio": 1687 case "select": { 1688 const values = normalizeFieldValues(fieldValue); 1689 return values.includes(criterion.value); 1690 } 1691 default: 1692 return true; 1693 } 1694 } 1695 1696 function updateSearchtextAndCategoriesFilters() { 1697 // Read searchtext filter 1698 const markerFilterInput = document.getElementById("oum_filter_markers"); 1699 searchtextFilterValue = markerFilterInput ? markerFilterInput.value.toLowerCase() : ""; 1700 1701 // Read categories filter 1702 const categoryInputs = document.querySelectorAll(categoryFilterSelector); 1703 if (categoryInputs.length === 0) { 1704 // No category UI on this map (hidden overlay / hide_filterbox) â do not filter by category 1705 categoriesFilterSelection = null; 1706 } else { 1707 categoriesFilterSelection = new Set( 1708 Array.from(categoryInputs) 1709 .filter((input) => input.checked) 1710 .map((input) => input.value) 1711 ); 1712 } 1713 1714 applyFilters(); 1715 } 1716 1717 function setupFilterListEvents() { 1718 const filterControls = document.querySelector( 1719 ".open-user-map .oum-filter-controls" 1720 ); 1721 if (!filterControls) return; 1722 1723 const closeButton = filterControls.querySelector(".close-filter-list"); 1724 const toggle = filterControls.querySelector(".oum-filter-toggle"); 1725 const filterWrapper = filterControls.closest(".oum-map-filter-wrapper"); 1726 1727 // Close button always collapses the filter controls 1728 if (closeButton) { 1729 closeButton.addEventListener("click", function(e) { 1730 e.stopPropagation(); 1731 filterControls.classList.remove("active"); 1732 // Ensure use-collapse class is present so the toggle button becomes visible 1733 filterControls.classList.add("use-collapse"); 1734 }); 1735 } 1736 1737 // Setup click toggle for the toggle button (works regardless of initial use-collapse state) 1738 if (toggle) { 1739 toggle.addEventListener("click", function(e) { 1740 e.stopPropagation(); 1741 filterControls.classList.toggle("active"); 1742 }); 1743 } 1744 1745 // Close when clicking outside the filter wrapper (only if use-collapse is present) 1746 if (filterWrapper) { 1747 document.addEventListener("click", function(e) { 1748 if (filterControls.classList.contains("use-collapse") && !filterWrapper.contains(e.target)) { 1749 filterControls.classList.remove("active"); 1750 } 1751 }); 1752 } 1753 } 1754 1755 // Select all works for both the on-map overlay and the Advanced Filter section 1756 function setupToggleAll() { 1757 const categoryFilter = document.querySelector( 1758 ".open-user-map .oum-marker-categories-filter" 1759 ); 1760 if (!categoryFilter) return; 1761 1762 const toggleAllCheckbox = categoryFilter.querySelector( 1763 ".oum-toggle-all-checkbox" 1764 ); 1765 const categoryCheckboxes = categoryFilter.querySelectorAll( 1766 '[name="type"]' 1767 ); 1768 1769 if (!toggleAllCheckbox || categoryCheckboxes.length === 0) return; 1770 1771 function updateToggleAllState() { 1772 const checkedCount = Array.from(categoryCheckboxes).filter( 1773 (cb) => cb.checked 1774 ).length; 1775 const totalCount = categoryCheckboxes.length; 1776 1777 if (checkedCount === 0) { 1778 toggleAllCheckbox.checked = false; 1779 toggleAllCheckbox.indeterminate = false; 1780 } else if (checkedCount === totalCount) { 1781 toggleAllCheckbox.checked = true; 1782 toggleAllCheckbox.indeterminate = false; 1783 } else { 1784 toggleAllCheckbox.checked = false; 1785 toggleAllCheckbox.indeterminate = true; 1786 } 1787 } 1788 1789 function toggleAllCategories() { 1790 const checkedCount = Array.from(categoryCheckboxes).filter( 1791 (cb) => cb.checked 1792 ).length; 1793 const totalCount = categoryCheckboxes.length; 1794 const shouldCheck = checkedCount < totalCount; 1795 1796 categoryCheckboxes.forEach((checkbox) => { 1797 checkbox.checked = shouldCheck; 1798 }); 1799 1800 updateSearchtextAndCategoriesFilters(); 1801 } 1802 1803 toggleAllCheckbox.addEventListener("change", toggleAllCategories); 1804 1805 categoryCheckboxes.forEach((checkbox) => { 1806 checkbox.addEventListener("change", updateToggleAllState); 1807 }); 1808 1809 updateToggleAllState(); 1810 } 1811 1812 function resetCategoriesFilter() { 1813 const categoryInputs = document.querySelectorAll(categoryFilterSelector);
1814 categoryInputs.forEach((input) => { 1815 input.checked = true; 1816 }); 1817 1818 const toggleAllCheckbox = document.querySelector( 1819 ".open-user-map .oum-marker-categories-filter .oum-toggle-all-checkbox" 1820 ); 1821 if (toggleAllCheckbox) { 1822 toggleAllCheckbox.checked = true; 1823 toggleAllCheckbox.indeterminate = false; 1824 } 1825 1826 updateSearchtextAndCategoriesFilters(); 1827 } 1828 1829 function handleAutoOpenMarker(markerId) { 1830 const targetMarker = allMarkers.find((m) => m.options.post_id === markerId); 1831 if (targetMarker) { 1832 const openPopup = typeof oum_hide_location_popup === "undefined" || !oum_hide_location_popup; 1833 if (oum_enable_cluster) { 1834 if (targetMarker instanceof L.Marker && typeof markersLayer.zoomToShowLayer === "function") { 1835 setTimeout(() => { 1836 markersLayer.zoomToShowLayer(targetMarker, () => { 1837 if (openPopup) targetMarker.openPopup(); 1838 }); 1839 }, 500); 1840 } else if (targetMarker.getBounds) { 1841 map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] }); 1842 if (openPopup) targetMarker.openPopup(); 1843 } 1844 } else { 1845 if (targetMarker.getLatLng) {
1846 map.setView(targetMarker.getLatLng(), targetMarker.options.zoom); 1847 } else if (targetMarker.getBounds) { 1848 map.fitBounds(targetMarker.getBounds(), { padding: [20, 20] }); 1849 } 1850 if (openPopup) targetMarker.openPopup(); 1851 } 1852 } 1853 } 1854 1855 // Public interface 1856 return { 1857 init: function (mapInstance) { 1858 map = mapInstance; 1859 initializeMarkersLayer(); 1860 markersLayer.addTo(map); 1861 vectorLayer.addTo(map); 1862 setupMarkerEvents(); 1863 setupFilterListEvents(); 1864 setupToggleAll(); 1865 1866 // Make layer globally available (for backward compatibility) 1867 window.oumMarkersLayer = markersLayer; 1868 window.oumAllMarkers = allMarkers; 1869 1870 // Dispatch markers initialized event 1871 document.dispatchEvent(new CustomEvent('oum:markers_initialized', { 1872 detail: { 1873 mapId: map._container.id, 1874 markersLayer: markersLayer 1875 } 1876 })); 1877 1878 return this; 1879 }, 1880 addMarkers: function (locations) { 1881 locations.forEach((location) => { 1882 const marker = createLocationLayer(location); 1883 allMarkers.push(marker); 1884 locationsById[String(location.post_id)] = location; 1885 }); 1886 1887 // Apply markers once so filters and clustering can batch-add visible markers. 1888 applyFilters(); 1889 1890 // After adding all markers, check if we need to auto-open one 1891 const POPUP_MARKER_ID = OUMUtils.getParameterByName("markerid"); 1892 if (POPUP_MARKER_ID) { 1893 handleAutoOpenMarker(POPUP_MARKER_ID); 1894 } 1895 }, 1896 updateSearchtextAndCategoriesFilters: updateSearchtextAndCategoriesFilters, 1897 resetCategoriesFilter: resetCategoriesFilter, 1898 getMarkersLayer: function () { 1899 return markersLayer; 1900 }, 1901 getAllMarkers: function () { 1902 return allMarkers; 1903 }, 1904 getAllLocations: function () { 1905 return Object.values(locationsById); 1906 }, 1907 setCustomfieldsFilter: function (criteria) { 1908 if (criteria && Object.keys(criteria).length > 0) { 1909 customfieldsFilterSelection = criteria; 1910 } else { 1911 customfieldsFilterSelection = null; 1912 } 1913 applyFilters(); 1914 }, 1915 getFilterState: function () { 1916 return { 1917 searchtext: searchtextFilterValue, 1918 categories: 1919 categoriesFilterSelection === null 1920 ? null 1921 : Array.from(categoriesFilterSelection), 1922 customfields: customfieldsFilterSelection 1923 }; 1924 }, 1925 getFilteredMarkersCount: function () { 1926 return visibleMarkersCount; 1927 }, 1928 /** Clear lazy-loaded popup HTML so the next open refetches (e.g. after vote count changes). */ 1929 invalidateBubbleHtmlCacheForPost: function (postId) { 1930 const id = String(postId); 1931 allMarkers.forEach((m) => { 1932 if (m && m.options && String(m.options.post_id) === id) { 1933 delete m._oumBubbleHtmlCache; 1934 m._oumBubbleFetchId = 0; 1935 } 1936 }); 1937 }, 1938 }; 1939})(); 1940 1941/** 1942 * Form Map Module - Handles all map-related functionality for the form 1943 */ 1944const OUMFormMap = (function () { 1945 // Private variables 1946 let formMap = null; 1947 let locationMarker = null; 1948 let markerIsVisible = false; 1949 let isAdjusting = false; 1950 let isInitialized = false; 1951 let currentGeometryType = "point"; 1952 let polylineDrawnItems = null; 1953 let polylineDrawControl = null; 1954 let polylineLayer = null; 1955 1956 // Private functions 1957 function initializeFormMap() { 1958 if (isInitialized) { 1959 return; 1960 } 1961 1962 formMap = L.map("mapGetLocation", { 1963 attributionControl: false, 1964 gestureHandling: true, 1965 minZoom: 1, 1966 zoomSnap: 0.01, // Allow fractional zoom levels (0.01 steps) 1967 zoomDelta: 1, // Zoom step size for controls 1968 fullscreenControl: oum_enable_fullscreen, 1969 fullscreenControlOptions: { 1970 position: "topleft", 1971 }, 1972 }); 1973 1974 // Make form map globally available (for backward compatibility) 1975 window.oumMap2 = formMap; 1976 1977 setupTileLayer(); 1978 setupControls(); 1979 setupMarker(); 1980 setupMapEvents(); 1981 setupPolylineDrawing(); 1982 setupLocationTypeControls(); 1983 1984 // Invalidate size to ensure accurate measurements before calculating bounds 1985 formMap.invalidateSize(); 1986 1987 // Always apply bounds to prevent showing repeated maps 1988 const boundsToUse = oum_enable_fixed_map_bounds 1989 ? sharedMapBounds 1990 : OUMConfig.defaults.map.bounds; 1991 1992 // Set the bounds 1993 formMap.setMaxBounds(boundsToUse); 1994 1995 // Calculate minimum zoom level based on bounds
1996 // When fixed bounds are enabled, use sharedMapBounds for accurate calculation 1997 // This is used both for setting minZoom and in the moveend event handler 1998 let maxVisibleBounds; 1999 if (oum_enable_fixed_map_bounds && sharedMapBounds) { 2000 // Invalidate size first to ensure accurate measurements 2001 formMap.invalidateSize(); 2002 maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds); 2003 // Set minimum zoom level to prevent users from zooming out too far 2004 formMap.setMinZoom(maxVisibleBounds); 2005 } else { 2006 maxVisibleBounds = formMap.getBoundsZoom(boundsToUse); 2007 } 2008 2009 // Add moveend event to enforce bounds 2010 formMap.on("moveend", function () { 2011 if (isAdjusting) return; 2012 isAdjusting = true; 2013 2014 const zoom = formMap.getZoom(); 2015 2016 // Only enforce bounds if we're zoomed in beyond the minimum zoom 2017 if (zoom > maxVisibleBounds) { 2018 const currentBounds = formMap.getBounds(); 2019 const currentCenter = formMap.getCenter(); 2020 2021 let needsAdjustment = false; 2022 let newLat = currentCenter.lat; 2023 let newLng = currentCenter.lng; 2024 2025 // Calculate current viewport dimensions 2026 const viewportHeight = currentBounds.getNorth() - currentBounds.getSouth(); 2027 const viewportWidth = currentBounds.getEast() - currentBounds.getWest(); 2028 2029 // Check and adjust latitude (north/south) 2030 if (currentBounds.getNorth() > boundsToUse.getNorth()) { 2031 newLat = boundsToUse.getNorth() - viewportHeight / 2; 2032 needsAdjustment = true; 2033 } else if (currentBounds.getSouth() < boundsToUse.getSouth()) { 2034 newLat = boundsToUse.getSouth() + viewportHeight / 2; 2035 needsAdjustment = true; 2036 } 2037 2038 // Check and adjust longitude (east/west) 2039 if (currentBounds.getEast() > boundsToUse.getEast()) { 2040 newLng = boundsToUse.getEast() - viewportWidth / 2; 2041 needsAdjustment = true; 2042 } else if (currentBounds.getWest() < boundsToUse.getWest()) { 2043 newLng = boundsToUse.getWest() + viewportWidth / 2; 2044 needsAdjustment = true; 2045 } 2046 2047 if (needsAdjustment) { 2048 formMap.setView([newLat, newLng], zoom, { animate: false }); 2049 } 2050 } 2051 2052 isAdjusting = false; 2053 }); 2054 2055 // Keep the hidden zoom input in sync with the current form map zoom. 2056 formMap.on("zoomend", function () { 2057 syncLocationZoomField(); 2058 }); 2059 2060 // Update minZoom when fixed bounds are enabled 2061 // This prevents users from zooming out too far 2062 updateMinZoom(); 2063 2064 isInitialized = true; 2065 } 2066 2067 function updateMinZoom() { 2068 if (!formMap) return; 2069 2070 // Recalculate and set minZoom when fixed bounds are enabled 2071 // This prevents users from zooming out beyond the defined bounds 2072 if (oum_enable_fixed_map_bounds && sharedMapBounds) { 2073 formMap.invalidateSize(); 2074 const maxVisibleBounds = formMap.getBoundsZoom(sharedMapBounds); 2075 formMap.setMinZoom(maxVisibleBounds); 2076 } 2077 } 2078 2079 function setupTileLayer() { 2080 // Default to OpenStreetMap if mapStyle is undefined 2081 const mapStyle = window.mapStyle || 'OpenFreeMap.bright'; 2082 2083 window.OUMMapStyles.addBaseLayer(formMap, mapStyle, { 2084 mapboxKey: OUMConfig.getTileProviderKey(), 2085 cartoKey: OUMConfig.getCartoTileProviderKey(), 2086 setupCustomImageLayer: setupCustomImageLayer, 2087 customImageHideTiles: window.oum_custom_image_hide_tiles, 2088 customImageBackgroundColor: window.oum_custom_image_background_color, 2089 }); 2090 } 2091 2092 function setupControls() { 2093 // Add searchbar: address 2094 const search = new GeoSearch.GeoSearchControl({ 2095 style: "bar", 2096 showMarker: false, 2097 provider: OUMUtils.initGeosearchProvider(), 2098 searchLabel: oum_searchaddress_label, 2099 updateMap: false, 2100 }); 2101 formMap.addControl(search); 2102 2103 // Add control: get current location 2104 if (oum_enable_currentlocation) { 2105 window.formMap_locate_process = L.control.locate({ 2106 flyTo: true, 2107 showPopup: false, 2108 }).addTo(formMap); 2109 } 2110 } 2111 2112 function setupMarker() { 2113 // Marker Icon 2114 let markerIcon = L.icon({ 2115 iconUrl: marker_icon_url, 2116 iconSize: [26, 41], 2117 iconAnchor: [13, 41], 2118 popupAnchor: [0, -25], 2119 shadowUrl: marker_shadow_url, 2120 shadowSize: [41, 41], 2121 shadowAnchor: [13, 41], 2122 }); 2123 2124 locationMarker = L.marker([0, 0], { 2125 icon: markerIcon, 2126 draggable: true, 2127 }); 2128 2129 // Make marker globally available (for backward compatibility) 2130 window.locationMarker = locationMarker; 2131 window.markerIsVisible = markerIsVisible; 2132 2133 // Event: drag marker 2134 locationMarker.on("dragend", function (e) { 2135 setLocationLatLng(e.target.getLatLng()); 2136 }); 2137 } 2138 2139 function setupMapEvents() { 2140 // Event: click on map to set marker OR location found 2141 formMap.on("click locationfound", function (e) { 2142 if (currentGeometryType !== "point") { 2143 return; 2144 } 2145 2146 let coords = e.latlng; 2147 locationMarker.setLatLng(coords); 2148 2149 if (!markerIsVisible) { 2150 locationMarker.addTo(formMap); 2151 markerIsVisible = true; 2152 window.markerIsVisible = true; 2153 } 2154 2155 setLocationLatLng(coords); 2156 }); 2157 2158 // Event: geosearch success 2159 formMap.on("geosearch/showlocation", handleGeosearchSuccess); 2160 } 2161 2162 function handleGeosearchSuccess(e) {
2163 let coords = e.marker._latlng; 2164 let label = e.location.label; 2165 const searchBar = document.querySelector(`#mapGetLocation .leaflet-geosearch-bar form`); 2166 2167 // Check against fixed map bounds if enabled, otherwise allow any location 2168 let isInBounds = true; // Default to true when fixed bounds is disabled 2169 if (oum_enable_fixed_map_bounds === 'on' && sharedMapBounds) { 2170 // Check if location is within the fixed map bounds (not just current viewport) 2171 isInBounds = sharedMapBounds.contains(coords); 2172 } 2173 2174 // Only restrict movement if fixed map bounds is enabled and location is outside those bounds 2175 if (!isInBounds && oum_enable_fixed_map_bounds === 'on') { 2176 console.log("This search result is out of reach."); 2177 if (searchBar) { 2178 searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)"; 2179 setTimeout(function () { 2180 searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)"; 2181 }, 2000); 2182 } 2183 } else { 2184 if (currentGeometryType !== "point") { 2185 handleValidGeosearchResult(e.location); 2186 return; 2187 } 2188
2189 // Location is within fixed bounds (or fixed bounds is disabled) - handle normally 2190 // Set coordinates and address immediately (before map animation) for instant feedback 2191 setLocationLatLng(coords, label); 2192 2193 // Set marker position 2194 locationMarker.setLatLng(coords); 2195 2196 if (!markerIsVisible) { 2197 locationMarker.addTo(formMap); 2198 markerIsVisible = true; 2199 window.markerIsVisible = true; 2200 } 2201 2202 // Fly to location (animated - happens after address is already set) 2203 handleValidGeosearchResult(e.location); 2204 } 2205 } 2206 2207 function handleValidGeosearchResult(location) { 2208 if (location.bounds !== null) { 2209 formMap.flyToBounds(location.bounds); 2210 } else if (location.raw.mapView) { 2211 formMap.flyToBounds([ 2212 [location.raw.mapView.south, location.raw.mapView.west], 2213 [location.raw.mapView.north, location.raw.mapView.east], 2214 ]); 2215 } else { 2216 formMap.flyTo([location.y, location.x], 17); 2217 } 2218 } 2219 2220 function setLocationLatLng(markerLatLng, address, skipReverseGeocode = false) { 2221 document.getElementById("oum_location_lat").value = markerLatLng.lat; 2222 document.getElementById("oum_location_lng").value = markerLatLng.lng; 2223 syncLocationZoomField(); 2224 2225 // Only perform reverse geocoding if both subtitle field and autofill are enabled 2226 if (!skipReverseGeocode && shouldPerformReverseGeocoding()) { 2227 reverseGeocode(markerLatLng.lat, markerLatLng.lng, address); 2228 } 2229 } 2230 2231 function clearMarker() { 2232 if (locationMarker && formMap) { 2233 locationMarker.remove(); 2234 markerIsVisible = false; 2235 window.markerIsVisible = false; 2236 } 2237 } 2238 2239 function setupLocationTypeControls() { 2240 const controls = document.querySelectorAll('input[name="oum_location_type_choice"]'); 2241 controls.forEach((control) => { 2242 control.addEventListener("change", function () { 2243 if (this.checked) { 2244 setGeometryType(this.value); 2245 } 2246 }); 2247 }); 2248 2249 const typeField = document.getElementById("oum_geometry_type"); 2250 setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false }); 2251 } 2252 2253 function setupPolylineDrawing() { 2254 if (!L.Control || !L.Control.Draw) { 2255 return; 2256 } 2257 2258 polylineDrawnItems = L.featureGroup().addTo(formMap); 2259 createPolylineDrawControl(); 2260 setupPolylineDrawingEvents(); 2261 } 2262 2263 function createPolylineDrawControl() { 2264 if (!formMap || !L.Control || !L.Control.Draw) { 2265 return; 2266 } 2267 2268 if (polylineDrawControl && polylineDrawControl._map) { 2269 formMap.removeControl(polylineDrawControl); 2270 } 2271 2272 polylineDrawControl = new L.Control.Draw({ 2273 draw: { 2274 polyline: currentGeometryType === "polyline" ? { 2275 shapeOptions: getPolylineStyle(), 2276 } : false, 2277 polygon: currentGeometryType === "polygon" ? { 2278 shapeOptions: getPolygonStyle(), 2279 } : false, 2280 rectangle: false, 2281 circle: false, 2282 circlemarker: false, 2283 marker: false, 2284 }, 2285 edit: { 2286 featureGroup: polylineDrawnItems, 2287 remove: true, 2288 }, 2289 }); 2290 } 2291 2292 function setupPolylineDrawingEvents() { 2293 formMap.on(L.Draw.Event.CREATED, function (event) { 2294 if (currentGeometryType === "point") { 2295 return; 2296 } 2297 2298 setPolylineLayer(event.layer); 2299 }); 2300 2301 formMap.on(L.Draw.Event.EDITED, function () { 2302 syncPolylineGeometryFields(); 2303 }); 2304 2305 formMap.on(L.Draw.Event.DELETED, function () { 2306 polylineLayer = null; 2307 syncPolylineGeometryFields(); 2308 }); 2309 } 2310 2311 function getPolylineStyle() { 2312 return { 2313 color: "#e82c71", 2314 weight: 4, 2315 opacity: 0.9, 2316 }; 2317 } 2318 2319 function getPolygonStyle() { 2320 return { 2321 ...getPolylineStyle(), 2322 fillColor: "#e82c71", 2323 fillOpacity: 0.25, 2324 }; 2325 } 2326 2327 function setGeometryType(type, options = {}) { 2328 currentGeometryType = type === "polyline" || type === "polygon" ? type : "point"; 2329 const autoStartDrawing = options.autoStartDrawing !== false; 2330 2331 cancelActiveDrawTool(); 2332 2333 const addLocationEl = document.querySelector(".add-location"); 2334 if (addLocationEl) { 2335 addLocationEl.setAttribute("data-oum-geometry-type", currentGeometryType); 2336 } 2337 2338 const typeField = document.getElementById("oum_geometry_type"); 2339 if (typeField) { 2340 typeField.value = currentGeometryType; 2341 } 2342 2343 const choice = document.querySelector(`input[name="oum_location_type_choice"][value="${currentGeometryType}"]`); 2344 if (choice) { 2345 choice.checked = true; 2346 } 2347 2348 document.querySelectorAll("[data-oum-location-type-card]").forEach((card) => { 2349 card.classList.toggle("is-selected", card.getAttribute("data-oum-location-type-card") === currentGeometryType); 2350 }); 2351 2352 updateLocationTypeCategoryUi(); 2353 2354 if (currentGeometryType === "polyline") { 2355 clearMarker(); 2356 createPolylineDrawControl(); 2357 addPolylineControl(); 2358 if (autoStartDrawing) { 2359 startActiveDrawTool(); 2360 } 2361 } else if (currentGeometryType === "polygon") { 2362 clearMarker(); 2363 createPolylineDrawControl(); 2364 addPolylineControl(); 2365 if (autoStartDrawing) { 2366 startActiveDrawTool(); 2367 } 2368 } else { 2369 removePolylineControl(); 2370 clearPolyline(); 2371 } 2372 2373 syncPolylineGeometryFields(); 2374 } 2375 2376 function updateLocationTypeCategoryUi() { 2377 const categoryLabelKey = currentGeometryType === "polygon" 2378 ? "polygonLabel" 2379 : (currentGeometryType === "polyline" ? "polylineLabel" : "pointLabel"); 2380 const categoryTitles = document.querySelectorAll("[data-oum-category-title]"); 2381
2382 categoryTitles.forEach((titleEl) => { 2383 const label = titleEl.dataset[categoryLabelKey] || titleEl.dataset.pointLabel || titleEl.textContent; 2384 titleEl.textContent = label; 2385 }); 2386 2387 document.querySelectorAll(".oum-category-dropdown-display").forEach((display) => { 2388 display.setAttribute("data-oum-geometry-type", currentGeometryType); 2389 }); 2390 2391 document.querySelectorAll(".oum-marker-category-option").forEach((option) => { 2392 const categoryType = option.getAttribute("data-category-type") || "point"; 2393 const isMatchingType = categoryType === currentGeometryType; 2394 const input = option.querySelector('input[name="oum_marker_icon[]"]'); 2395 2396 option.hidden = !isMatchingType; 2397 2398 if (input) { 2399 input.disabled = !isMatchingType; 2400 if (!isMatchingType) { 2401 input.checked = false; 2402 } 2403 } 2404 }); 2405 2406 document.querySelectorAll('select#oum_marker_icon option[data-category-type]').forEach((option) => { 2407 const categoryType = option.getAttribute("data-category-type") || "point"; 2408 const isMatchingType = categoryType === currentGeometryType; 2409 2410 option.hidden = !isMatchingType; 2411 option.disabled = !isMatchingType; 2412 if (!isMatchingType && option.selected) { 2413 option.selected = false; 2414 } 2415 }); 2416 2417 document.querySelectorAll("select#oum_marker_icon").forEach((select) => { 2418 const selectedOption = select.options[select.selectedIndex]; 2419 if (selectedOption && selectedOption.disabled) { 2420 select.value = ""; 2421 } 2422 select.dispatchEvent(new Event("change")); 2423 }); 2424 } 2425 2426 function startActiveDrawTool(attempt = 0) { 2427 if (!formMap) { 2428 return; 2429 } 2430 2431 window.setTimeout(() => { 2432 if (!formMap._loaded) { 2433 if (attempt < 10) { 2434 startActiveDrawTool(attempt + 1); 2435 } 2436 return; 2437 } 2438 2439 const mapContainer = formMap.getContainer(); 2440 if (!mapContainer) { 2441 return; 2442 } 2443 2444 const selector = currentGeometryType === "polygon" 2445 ? ".leaflet-draw-draw-polygon" 2446 : ".leaflet-draw-draw-polyline"; 2447 const drawButton = mapContainer.querySelector(selector); 2448 2449 if ( 2450 drawButton && 2451 !drawButton.classList.contains("leaflet-disabled") && 2452 !drawButton.classList.contains("leaflet-draw-toolbar-button-enabled") 2453 ) { 2454 drawButton.click(); 2455 } 2456 }, 0); 2457 } 2458 2459 function startDrawToolForCurrentType() { 2460 if (currentGeometryType === "polyline" || currentGeometryType === "polygon") { 2461 startActiveDrawTool(); 2462 } 2463 } 2464 2465 function startEditLayersTool() { 2466 if (!formMap || !polylineLayer) { 2467 return; 2468 } 2469 2470 window.setTimeout(() => { 2471 const mapContainer = formMap.getContainer(); 2472 if (!mapContainer) { 2473 return; 2474 } 2475 2476 const editButton = mapContainer.querySelector(".leaflet-draw-edit-edit"); 2477 const editIsActive = editButton && editButton.classList.contains("leaflet-draw-toolbar-button-enabled"); 2478 const editIsDisabled = editButton && editButton.classList.contains("leaflet-disabled"); 2479 2480 if (editButton && !editIsActive && !editIsDisabled) { 2481 editButton.click(); 2482 } 2483 }, 0); 2484 } 2485 2486 function cancelActiveDrawTool() { 2487 if (!formMap) { 2488 return; 2489 } 2490 2491 const mapContainer = formMap.getContainer(); 2492 if (!mapContainer) { 2493 return; 2494 } 2495 2496 const cancelButton = mapContainer.querySelector(".leaflet-draw-actions a[title='Cancel drawing'], .leaflet-draw-actions a[title='Cancel']"); 2497 if (cancelButton) { 2498 cancelButton.click(); 2499 } 2500 } 2501 2502 function addPolylineControl() { 2503 if (polylineDrawControl && formMap && !polylineDrawControl._map) { 2504 formMap.addControl(polylineDrawControl); 2505 } 2506 } 2507 2508 function removePolylineControl() { 2509 if (polylineDrawControl && formMap && polylineDrawControl._map) { 2510 formMap.removeControl(polylineDrawControl); 2511 } 2512 } 2513 2514 function setPolylineLayer(layer) { 2515 if (!polylineDrawnItems || !layer) { 2516 return; 2517 } 2518 2519 clearPolyline(); 2520 polylineLayer = layer; 2521 if (polylineLayer.setStyle) { 2522 polylineLayer.setStyle(currentGeometryType === "polygon" ? getPolygonStyle() : getPolylineStyle()); 2523 } 2524 polylineDrawnItems.addLayer(polylineLayer); 2525 syncPolylineGeometryFields(); 2526 } 2527 2528 function clearPolyline() { 2529 if (polylineDrawnItems) { 2530 polylineDrawnItems.clearLayers(); 2531 } 2532 polylineLayer = null; 2533 } 2534 2535 function setPolylineGeometry(geometry) { 2536 if (!geometry || !Array.isArray(geometry.coordinates) || (geometry.type !== "LineString" && geometry.type !== "Polygon")) { 2537 clearPolyline(); 2538 syncPolylineGeometryFields(); 2539 return; 2540 } 2541 2542 const rawCoordinates = geometry.type === "Polygon" && Array.isArray(geometry.coordinates[0]) 2543 ? geometry.coordinates[0] 2544 : geometry.coordinates; 2545 const latLngs = rawCoordinates 2546 .filter((coordinate) => Array.isArray(coordinate) && coordinate.length >= 2) 2547 .map((coordinate) => L.latLng(Number(coordinate[1]), Number(coordinate[0]))); 2548 2549 if (latLngs.length < 2 || (geometry.type === "Polygon" && latLngs.length < 4)) { 2550 clearPolyline(); 2551 syncPolylineGeometryFields(); 2552 return; 2553 } 2554 2555 setGeometryType(geometry.type === "Polygon" ? "polygon" : "polyline", { autoStartDrawing: false }); 2556 setPolylineLayer(geometry.type === "Polygon" ? L.polygon(latLngs, getPolygonStyle()) : L.polyline(latLngs, getPolylineStyle())); 2557 fitPolylineBounds(); 2558 } 2559 2560 function fitPolylineBounds() { 2561 if (!formMap || !polylineLayer || !polylineLayer.getBounds) { 2562 return; 2563 } 2564 2565 const bounds = polylineLayer.getBounds(); 2566 if (bounds && bounds.isValid()) { 2567 formMap.fitBounds(bounds, { padding: [20, 20] }); 2568 } 2569 } 2570 2571 function syncPolylineGeometryFields() { 2572 const geometryField = document.getElementById("oum_location_geometry"); 2573 2574 if (!geometryField) { 2575 return; 2576 } 2577 2578 if (currentGeometryType === "point" || !polylineLayer) { 2579 geometryField.value = ""; 2580 return; 2581 } 2582 2583 const geometry = polylineLayer.toGeoJSON().geometry; 2584 geometryField.value = JSON.stringify(geometry); 2585 2586 const bounds = polylineLayer.getBounds(); 2587 if (bounds && bounds.isValid()) { 2588 setLocationLatLng(bounds.getCenter(), "", true); 2589 } 2590 } 2591 2592 function syncLocationZoomField() { 2593 const zoomField = document.getElementById("oum_location_zoom"); 2594 if (!zoomField || !formMap) { 2595 return; 2596 } 2597 zoomField.value = formMap.getZoom(); 2598 } 2599 2600 /** 2601 * Check if reverse geocoding should be performed 2602 * Requires both subtitle field and autofill to be enabled 2603 * @returns {boolean} 2604 */ 2605 function shouldPerformReverseGeocoding() { 2606 // Check if subtitle field is enabled (use window prefix for safety) 2607 const subtitleEnabled = typeof window.oum_enable_address !== 'undefined' && window.oum_enable_address === 'on'; 2608 2609 // Check if autofill is enabled (use window prefix for safety) 2610 const autofillEnabled = typeof window.oum_enable_address_autofill !== 'undefined' && window.oum_enable_address_autofill === 'on'; 2611 2612 return subtitleEnabled && autofillEnabled; 2613 } 2614 2615 /** 2616 * Format address from Nominatim API response 2617 * @param {Object} addressData - Address data from Nominatim API 2618 * @param {string} displayName - Fallback display name 2619 * @returns {string} 2620 */
2621 function formatAddressFromResponse(addressData, displayName) { 2622 const addressParts = []; 2623 2624 // Build address from most specific to least specific 2625 if (addressData.house_number) addressParts.push(addressData.house_number); 2626 if (addressData.road) addressParts.push(addressData.road); 2627 if (addressData.suburb) addressParts.push(addressData.suburb); 2628 if (addressData.city || addressData.town || addressData.village) { 2629 addressParts.push(addressData.city || addressData.town || addressData.village); 2630 } 2631 if (addressData.postcode) addressParts.push(addressData.postcode); 2632 if (addressData.country) addressParts.push(addressData.country); 2633 2634 return addressParts.length > 0 2635 ? addressParts.join(', ') 2636 : displayName || 'Address not found'; 2637 } 2638 2639 /** 2640 * Reverse geocode coordinates to get address 2641 * Uses OpenStreetMap Nominatim API (free, no API key required) 2642 * @param {number} lat - Latitude 2643 * @param {number} lng - Longitude 2644 * @param {string} [providedAddress] - Optional address to use directly (e.g., from geosearch result) 2645 */ 2646 function reverseGeocode(lat, lng, providedAddress) { 2647 // If address is already provided (e.g., from geosearch), use it directly 2648 if (providedAddress && providedAddress.trim()) { 2649 handleAddressResult(providedAddress.trim()); 2650 return; 2651 } 2652 2653 // Validate coordinates 2654 if (!OUMUtils.validateCoordinates(lat, lng)) { 2655 console.warn('Open User Map: Invalid coordinates for reverse geocoding'); 2656 return; 2657 } 2658 2659 // Use Nominatim reverse geocoding API 2660 const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lng}&zoom=18&addressdetails=1`; 2661 2662 fetch(url, { 2663 headers: { 2664 'User-Agent': 'OpenUserMap/1.0' // Required by Nominatim 2665 } 2666 }) 2667 .then(response => { 2668 if (!response.ok) { 2669 throw new Error(`HTTP error! status: ${response.status}`); 2670 } 2671 return response.json(); 2672 }) 2673 .then(data => { 2674 if (data && data.address) { 2675 const address = formatAddressFromResponse(data.address, data.display_name); 2676 handleAddressResult(address); 2677 } else { 2678 console.log('Open User Map: No address found for coordinates:', lat, lng); 2679 } 2680 }) 2681 .catch(error => { 2682 console.warn('Open User Map: Reverse geocoding error:', error); 2683 }); 2684 } 2685 2686 /** 2687 * Handle the address result - log it and optionally fill subtitle field 2688 * @param {string} address - The formatted address 2689 */ 2690 function handleAddressResult(address) { 2691 if (!address || typeof address !== 'string') { 2692 return; 2693 } 2694 2695 console.log('Open User Map: Address:', address); 2696 2697 // Auto-fill subtitle field if enabled (always update when address changes) 2698 if (shouldPerformReverseGeocoding()) { 2699 const subtitleField = document.getElementById('oum_location_address'); 2700 if (subtitleField) { 2701 subtitleField.value = address; 2702 } 2703 } 2704 } 2705 2706 // Helper function to setup custom image layer 2707 function setupCustomImageLayer() { 2708 // Check if we have an image URL and bounds 2709 if (typeof window.oum_custom_image_url !== 'undefined' && window.oum_custom_image_url && 2710 typeof window.oum_custom_image_bounds !== 'undefined' && window.oum_custom_image_bounds) { 2711 2712 // Check if the uploaded file is an SVG 2713 const isSVG = window.oum_custom_image_url.toLowerCase().includes('.svg'); 2714 2715 if (isSVG) { 2716 // Handle SVG file - fetch and render as DOM elements 2717 setupSVGFromFile(); 2718 } else { 2719 // Handle regular image file 2720 setupImageOverlay(); 2721 } 2722 } else { 2723 } 2724 } 2725 2726 // Helper function to setup SVG from uploaded file 2727 function setupSVGFromFile() { 2728 try { 2729 // Get bounds data (now properly handled as object) 2730 const bounds = window.oum_custom_image_bounds; 2731 2732 // Validate bounds 2733 if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' || 2734 typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' || 2735 bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') { 2736 console.warn('Open User Map: Invalid or empty bounds data, skipping SVG file layer'); 2737 return; 2738 } 2739 2740 2741 // Fetch the SVG file and render it 2742 fetch(window.oum_custom_image_url) 2743 .then(response => response.text()) 2744 .then(svgText => { 2745 // Create SVG element from the fetched content 2746 const svgElement = createSVGElement(svgText); 2747 if (!svgElement) { 2748 console.warn('Open User Map: Cannot create SVG layer from file - invalid SVG element'); 2749 return; 2750 } 2751 2752 // Create a custom SVG layer 2753 const svgLayer = L.svgOverlay(svgElement, [ 2754 [bounds.north, bounds.west], // Southwest corner 2755 [bounds.south, bounds.east] // Northeast corner 2756 ], { 2757 opacity: 1.0, 2758 interactive: true 2759 }); 2760 2761 svgLayer.addTo(formMap); 2762 2763 2764 // Store reference for potential removal 2765 window.oumCustomSVGLayer = svgLayer; 2766 2767 console.log('Open User Map: Custom SVG file layer added successfully'); 2768 }) 2769 .catch(error => { 2770 console.warn('Open User Map: Error fetching SVG file:', error); 2771 }); 2772 2773 } catch (error) { 2774 console.warn('Open User Map: Error setting up custom SVG file layer:', error); 2775 } 2776 } 2777 2778 // Helper function to setup regular image overlay 2779 function setupImageOverlay() { 2780 try { 2781 // Get bounds data (now properly handled as object) 2782 const bounds = window.oum_custom_image_bounds; 2783 2784 // Validate bounds 2785 if (!bounds || typeof bounds.north === 'undefined' || typeof bounds.south === 'undefined' || 2786 typeof bounds.east === 'undefined' || typeof bounds.west === 'undefined' || 2787 bounds.north === '' || bounds.south === '' || bounds.east === '' || bounds.west === '') { 2788 console.warn('Open User Map: Invalid or empty bounds data, skipping image layer'); 2789 return; 2790 } 2791 2792 2793 // Create image overlay 2794 const imageOverlay = L.imageOverlay(window.oum_custom_image_url, [ 2795 [bounds.north, bounds.west], // Southwest corner 2796 [bounds.south, bounds.east] // Northeast corner 2797 ], { 2798 opacity: 1.0, 2799 interactive: true 2800 }); 2801 2802 imageOverlay.addTo(formMap); 2803 2804 2805 // Store reference for potential removal 2806 window.oumCustomImageLayer = imageOverlay; 2807 2808 console.log('Open User Map: Custom image layer added successfully'); 2809 2810 } catch (error) { 2811 console.warn('Open User Map: Error setting up custom image layer:', error); 2812 } 2813 } 2814 2815 // Helper function to create SVG element from text 2816 function createSVGElement(svgText) { 2817 // Create a temporary div to parse the SVG 2818 const tempDiv = document.createElement('div'); 2819 tempDiv.innerHTML = svgText; 2820 const svgElement = tempDiv.querySelector('svg'); 2821 2822 if (!svgElement) { 2823 console.warn('Open User Map: No valid SVG element found in SVG text'); 2824 return null; 2825 } 2826 2827 // Preserve the original viewBox if it exists 2828 // If missing, try to create it from width/height attributes 2829 if (!svgElement.getAttribute('viewBox')) { 2830 const width = svgElement.getAttribute('width'); 2831 const height = svgElement.getAttribute('height'); 2832 if (width && height) { 2833 // Remove units if present (e.g., "1580px" -> "1580") 2834 const widthNum = parseFloat(width); 2835 const heightNum = parseFloat(height); 2836 if (!isNaN(widthNum) && !isNaN(heightNum)) { 2837 svgElement.setAttribute('viewBox', `0 0 ${widthNum} ${heightNum}`); 2838 } else { 2839 svgElement.setAttribute('viewBox', '0 0 1000 1200'); 2840 } 2841 } else { 2842 svgElement.setAttribute('viewBox', '0 0 1000 1200'); 2843 } 2844 } 2845 2846 // Ensure the SVG has proper styling for overlay 2847 svgElement.style.width = '100%'; 2848 svgElement.style.height = '100%'; 2849 svgElement.style.display = 'block'; 2850 2851 // Ensure the SVG fills the entire bounds area to prevent cropping 2852 svgElement.setAttribute('preserveAspectRatio', 'none'); 2853 2854 console.log('Open User Map: SVG element created successfully:', svgElement); 2855 2856 return svgElement; 2857 } 2858 2859 // Public interface 2860 return { 2861 init: function() { 2862 if (!document.getElementById("mapGetLocation")) { 2863 return; 2864 } 2865 initializeFormMap(); 2866 },
2867 setLocation: function(lat, lng) { 2868 setGeometryType("point"); 2869 if (!locationMarker) return; 2870 locationMarker.setLatLng([lat, lng]); 2871 if (!markerIsVisible) { 2872 locationMarker.addTo(formMap); 2873 markerIsVisible = true; 2874 window.markerIsVisible = true; 2875 } 2876 setLocationLatLng({lat, lng}); 2877 }, 2878 clearMarker: function() { 2879 clearMarker(); 2880 }, 2881 setGeometryType: setGeometryType, 2882 updateLocationTypeCategoryUi: updateLocationTypeCategoryUi, 2883 setPolylineGeometry: setPolylineGeometry, 2884 fitPolylineBounds: fitPolylineBounds, 2885 startDrawToolForCurrentType: startDrawToolForCurrentType, 2886 startEditLayersTool: startEditLayersTool, 2887 clearPolyline: clearPolyline, 2888 syncGeometryFields: syncPolylineGeometryFields, 2889 invalidateSize: function() { 2890 if (formMap) { 2891 formMap.invalidateSize(); 2892 } 2893 }, 2894 updateMinZoom: function() { 2895 updateMinZoom(); 2896 }, 2897 getMap: function() { 2898 return formMap; 2899 }, 2900 setView: function(lat, lng, zoom) { 2901 if (formMap) { 2902 formMap.setView([lat, lng], zoom); 2903 syncLocationZoomField(); 2904 } 2905 },
2906 setViewToMatchMainMap: function(mainMap) { 2907 if (!formMap || !mainMap) return; 2908 2909 // Invalidate size to ensure accurate measurements 2910 formMap.invalidateSize(); 2911 2912 // Get the main map's bounds (the geographic area currently visible) 2913 const mainBounds = mainMap.getBounds(); 2914 const mainCenter = mainMap.getCenter(); 2915 const mainZoom = mainMap.getZoom(); 2916 2917 // Get form map container size 2918 const formMapContainer = formMap.getContainer(); 2919 const formMapWidth = formMapContainer.offsetWidth || 520; 2920 const formMapHeight = formMapContainer.offsetHeight || 294; 2921 2922 // Get main map container size 2923 const mainMapContainer = mainMap.getContainer(); 2924 const mainMapWidth = mainMapContainer.offsetWidth || 520; 2925 const mainMapHeight = mainMapContainer.offsetHeight || 294; 2926 2927 // Calculate aspect ratios 2928 const mainAspectRatio = mainMapWidth / mainMapHeight; 2929 const formAspectRatio = formMapWidth / formMapHeight; 2930 const aspectRatioDifference = Math.abs(mainAspectRatio - formAspectRatio); 2931 2932 let targetZoom; 2933 2934 // If aspect ratios match, use ratio-based zoom calculation 2935 if (aspectRatioDifference < 0.01) { 2936 // Calculate zoom adjustment based on size ratio 2937 const widthRatio = formMapWidth / mainMapWidth; 2938 const zoomAdjustment = Math.log2(widthRatio); 2939 targetZoom = mainZoom + zoomAdjustment; 2940 } else { 2941 // Different aspect ratios - use getBoundsZoom to fit the main map's bounds 2942 targetZoom = formMap.getBoundsZoom(mainBounds, false); 2943 } 2944 2945 // Set view with calculated zoom to show the same geographic area 2946 formMap.setView([mainCenter.lat, mainCenter.lng], targetZoom, { 2947 animate: false 2948 }); 2949 } 2950 }; 2951})(); 2952 2953/** 2954 * Form Controller Module - Handles all form-related functionality 2955 */ 2956const OUMFormController = (function () { 2957 // Private variables 2958 let isEditMode = false; 2959 let currentLocationId = null; 2960 let selectedFiles = []; 2961 2962 // Private functions 2963 function showFormMessage(type, headline, message, buttonText = null, buttonCallback = null) { 2964 const form = document.getElementById('oum_add_location'); 2965 const errorDiv = document.getElementById('oum_add_location_error'); 2966 const thankyouDiv = document.getElementById('oum_add_location_thankyou'); 2967 2968 if (!form || !errorDiv || !thankyouDiv) { 2969 console.error('Required form elements not found'); 2970 return; 2971 } 2972 2973 // Hide form and error 2974 form.style.display = 'none'; 2975 errorDiv.style.display = 'none'; 2976 2977 // Update thank you message 2978 const headlineEl = thankyouDiv.querySelector('h3'); 2979 const messageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text'); 2980 const buttonEl = thankyouDiv.querySelector('button'); 2981 2982 if (!headlineEl || !messageEl || !buttonEl) { 2983 // Create elements if they don't exist 2984 if (!headlineEl) { 2985 const newHeadline = document.createElement('h3'); 2986 thankyouDiv.appendChild(newHeadline); 2987 } 2988 if (!messageEl) { 2989 const newMessage = document.createElement('p'); 2990 newMessage.className = 'oum-add-location-thankyou-text'; 2991 thankyouDiv.appendChild(newMessage); 2992 } 2993 if (!buttonEl) { 2994 const newButton = document.createElement('button'); 2995 thankyouDiv.appendChild(newButton); 2996 } 2997 } 2998 2999 // Get elements again (they should exist now) 3000 const finalHeadlineEl = thankyouDiv.querySelector('h3'); 3001 const finalMessageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text'); 3002 const finalButtonEl = thankyouDiv.querySelector('button'); 3003 3004 // Add specific class for delete confirmation 3005 thankyouDiv.className = type === 'confirm-delete' ? 'oum-delete-confirmation' : ''; 3006 3007 if (finalHeadlineEl) finalHeadlineEl.textContent = headline || ''; 3008 if (finalMessageEl) finalMessageEl.textContent = message || ''; 3009 3010 // Handle button 3011 if (finalButtonEl) { 3012 if (buttonText && buttonCallback) { 3013 finalButtonEl.textContent = buttonText; 3014 finalButtonEl.onclick = buttonCallback; 3015 finalButtonEl.style.display = 'inline-block'; 3016 } else { 3017 finalButtonEl.style.display = 'none'; 3018 } 3019 } 3020 3021 thankyouDiv.style.display = 'block'; 3022 } 3023 3024 function setupDeleteButton() { 3025 const deleteBtn = document.getElementById('oum_delete_location_btn'); 3026 if (deleteBtn) { 3027 deleteBtn.addEventListener('click', function(e) { 3028 e.preventDefault(); 3029 3030 // Show confirmation using the message system 3031 showFormMessage( 3032 'confirm-delete', 3033 oum_custom_strings.delete_location, 3034 oum_custom_strings.delete_location_message, 3035 oum_custom_strings.delete_location_button, 3036 function() { 3037 // Set delete flag 3038 document.getElementById('oum_delete_location').value = 'true'; 3039 3040 // Get the form 3041 const form = document.getElementById('oum_add_location'); 3042 const formData = new FormData(form); 3043 formData.append('action', 'oum_add_location_from_frontend'); 3044 3045 // Submit via AJAX 3046 jQuery.ajax({ 3047 type: 'POST', 3048 url: oum_ajax.ajaxurl, 3049 cache: false, 3050 contentType: false, 3051 processData: false, 3052 data: formData, 3053 success: function(response) { 3054 if (response.success) { 3055 showFormMessage( 3056 'success', 3057 oum_custom_strings.location_deleted, 3058 oum_custom_strings.delete_success, 3059 oum_custom_strings.close_and_refresh, 3060 function() { 3061 window.location.reload(); 3062 } 3063 ); 3064 } else { 3065 oumShowError(response.data); 3066 } 3067 }, 3068 error: function(XMLHttpRequest, textStatus, errorThrown) { 3069 console.log(errorThrown); 3070 oumShowError([{message: oum_custom_strings.delete_error}]); 3071 } 3072 }); 3073 } 3074 ); 3075 }); 3076 } 3077 } 3078 3079 function setupFormEvents() { 3080 // Event: click on "+ Add Location" button 3081 const addLocationBtn = document.getElementById("open-add-location-overlay"); 3082 if (addLocationBtn) { 3083 addLocationBtn.addEventListener("click", handleAddLocationClick); 3084 } 3085 3086 // Event: click on "Edit Location" button 3087 document.addEventListener('click', function(e) { 3088 if (e.target && e.target.classList.contains('edit-location-button')) { 3089 e.preventDefault(); 3090 const locationId = e.target.getAttribute('data-post-id'); 3091 const location = window.oum_all_locations.find(loc => loc.post_id === locationId); 3092 3093 if (location) { 3094 resetForm(); 3095 openForm(location); 3096 } 3097 } 3098 }); 3099 3100 setupCloseEvents(); 3101 setupNotificationEvents(); 3102 setupMediaEvents(); 3103 } 3104 3105 /** 3106 * Request a fresh form nonce so cached pages stay valid. 3107 * 3108 * We refresh when the overlay opens, ensuring the submission 3109 * uses a nonce generated in the current time window. 3110 */ 3111 function refreshLocationNonce() { 3112 const formEl = document.getElementById('oum_add_location'); 3113 if (!formEl || typeof oum_ajax === 'undefined') { 3114 return Promise.resolve(); 3115 } 3116 3117 const nonceField = formEl.querySelector('input[name="oum_location_nonce"]'); 3118 if (!nonceField || !oum_ajax.ajaxurl) { 3119 return Promise.resolve(); 3120 } 3121 3122 const action = oum_ajax.refresh_nonce_action || 'oum_refresh_location_nonce'; 3123 3124 return jQuery.ajax({ 3125 type: 'POST', 3126 url: oum_ajax.ajaxurl, 3127 dataType: 'json', 3128 data: { action }, 3129 }).done(function(response) { 3130 if (response && response.success && response.data && response.data.nonce) { 3131 nonceField.value = response.data.nonce; 3132 } 3133 }).fail(function() { 3134 // Keep backward compatibility: fall back to the existing nonce if refresh fails. 3135 console.warn('Open User Map: Unable to refresh nonce.'); 3136 }); 3137 } 3138 3139 function handleAddLocationClick() { 3140 resetForm(); 3141 openForm(); 3142 } 3143 3144 function openForm(location = null) {
3145 // Refresh nonce on every open to avoid cached/expired tokens. 3146 refreshLocationNonce(); 3147 document.querySelector(".add-location").classList.add("active"); 3148 document.body.classList.add("oum-add-location-opened"); 3149 3150 setTimeout(function () { 3151 // Initialize map if needed 3152 OUMFormMap.init(); 3153 3154 // Update minZoom after form opens to ensure it's set correctly when fixed bounds are enabled 3155 // Use a small delay to ensure the map container is properly sized 3156 setTimeout(function() { 3157 OUMFormMap.updateMinZoom(); 3158 }, 100); 3159 3160 // Apply main map's aspect ratio to form map 3161 const applyAspectRatioAndView = () => { 3162 const mainMapEl = document.querySelector(`#${map_el}`); 3163 if (mainMapEl) { 3164 const mainMap = window.oumMap; 3165 if (mainMap) { 3166 const mainMapContainer = mainMap.getContainer(); 3167 const mainMapWidth = mainMapContainer.offsetWidth; 3168 const mainMapHeight = mainMapContainer.offsetHeight; 3169 3170 if (mainMapWidth && mainMapHeight) { 3171 // Calculate aspect ratio from main map 3172 const aspectRatio = mainMapWidth / mainMapHeight; 3173 3174 // Apply aspect ratio to form map container 3175 const formMapWrap = document.querySelector('.add-location .location-overlay-content .map-wrap'); 3176 if (formMapWrap) { 3177 formMapWrap.style.aspectRatio = aspectRatio.toString(); 3178 formMapWrap.style.height = 'auto'; 3179 } 3180 } 3181 } 3182 } 3183 3184 if (location) { 3185 populateForm(location); 3186 } else { 3187 // Set view to match main map's geographic bounds 3188 const mainMapEl = document.querySelector(`#${map_el}`); 3189 if (mainMapEl) { 3190 const mainMap = window.oumMap; 3191 // Use the new method that calculates zoom based on form map size 3192 // This ensures the same geographic area is visible regardless of map size differences 3193 OUMFormMap.setViewToMatchMainMap(mainMap); 3194 } 3195 } 3196 }; 3197 3198 // Wait for container to resize after applying aspect ratio 3199 applyAspectRatioAndView(); 3200 requestAnimationFrame(applyAspectRatioAndView); 3201 3202 // Add a separate timeout for invalidateSize to ensure accurate measurements 3203 setTimeout(() => { 3204 OUMFormMap.invalidateSize(); 3205 // Re-apply the view after size invalidation to ensure correct zoom 3206 if (location && (location.geometry_type === "polyline" || location.geometry_type === "polygon") && location.geometry) { 3207 OUMFormMap.fitPolylineBounds(); 3208 } else if (!location) { 3209 const mainMapEl = document.querySelector(`#${map_el}`); 3210 if (mainMapEl) { 3211 const mainMap = window.oumMap; 3212 OUMFormMap.setViewToMatchMainMap(mainMap); 3213 } 3214 OUMFormMap.startDrawToolForCurrentType(); 3215 } 3216 }, 200); 3217 }, 150); 3218 } 3219 3220 function setupCloseEvents() { 3221 const closeBtn = document.getElementById("close-add-location-overlay"); 3222 if (!closeBtn) return; 3223 3224 // Close button click 3225 closeBtn.addEventListener("click", closeForm); 3226 3227 // ESC key 3228 document.addEventListener("keydown", function(evt) { 3229 evt = evt || window.event; 3230 if (evt.key === "Escape" && document.getElementById("add-location-overlay").classList.contains("active")) { 3231 closeForm(); 3232 } 3233 }); 3234 3235 // Backdrop click 3236 document.getElementById("add-location-overlay").addEventListener("click", function(event) { 3237 if (event.target === this) { 3238 closeForm(); 3239 } 3240 }); 3241 } 3242 3243 function closeForm() { 3244 const addLocationOverlay = document.getElementById("add-location-overlay"); 3245 if (addLocationOverlay) { 3246 addLocationOverlay.classList.remove("active"); 3247 } 3248 3249 // Stop locate process 3250 if (window.formMap_locate_process) { 3251 window.formMap_locate_process.stop(); 3252 } 3253 3254 // Allow body scrolling 3255 document.querySelector("body").classList.remove("oum-add-location-opened"); 3256 3257 // Reset form and clear marker 3258 resetForm(); 3259 OUMFormMap.clearMarker(); 3260 } 3261 3262 function setupNotificationEvents() { 3263 const notificationCheckbox = document.getElementById("oum_location_notification"); 3264 if (notificationCheckbox) { 3265 notificationCheckbox.addEventListener("change", function() { 3266 const authorFields = document.getElementById("oum_author"); 3267 const nameField = document.getElementById("oum_location_author_name"); 3268 const emailField = document.getElementById("oum_location_author_email"); 3269 3270 if (this.checked) { 3271 authorFields.classList.add("active"); 3272 nameField.required = true; 3273 emailField.required = true; 3274 } else { 3275 authorFields.classList.remove("active"); 3276 nameField.required = false;
3277 emailField.required = false; 3278 } 3279 }); 3280 } 3281 } 3282 3283 function setupMediaEvents() { 3284 // Image upload 3285 const imageInput = document.getElementById("oum_location_images"); 3286 if (imageInput) { 3287 // Let OUMMedia handle the image upload 3288 OUMMedia.initializeImageUpload(imageInput); 3289 } 3290 3291 // Remove image button 3292 const removeImageBtn = document.getElementById("oum_remove_image"); 3293 if (removeImageBtn) { 3294 removeImageBtn.addEventListener("click", function() { 3295 document.getElementById("oum_location_images_preview").innerHTML = ""; 3296 }); 3297 } 3298 3299 // Remove audio button 3300 const removeAudioBtn = document.getElementById("oum_remove_audio"); 3301 if (removeAudioBtn) { 3302 removeAudioBtn.addEventListener("click", function() { 3303 const audioInput = document.getElementById("oum_location_audio"); 3304 const previewContainer = audioInput.nextElementSibling; 3305 const previewDiv = previewContainer.querySelector('.audio-preview'); 3306 3307 // Clear the file input 3308 audioInput.value = ""; 3309 3310 // Clear the preview 3311 if (previewDiv) { 3312 previewDiv.innerHTML = ''; 3313 } 3314 3315 // Remove active state 3316 previewContainer.classList.remove("active"); 3317 3318 // Set remove flag 3319 document.getElementById("oum_remove_existing_audio").value = "1"; 3320 }); 3321 } 3322 } 3323 3324 function setupCategoryDropdownIcons() { 3325 // Handle dropdown with icons for marker categories 3326 const categorySelect = document.querySelector('select#oum_marker_icon'); 3327 if (categorySelect) { 3328 // Create a custom dropdown wrapper 3329 const wrapper = document.createElement('div'); 3330 wrapper.className = 'oum-category-dropdown-wrapper'; 3331 wrapper.style.position = 'relative'; 3332 3333 // Insert wrapper before the select 3334 categorySelect.parentNode.insertBefore(wrapper, categorySelect); 3335 wrapper.appendChild(categorySelect); 3336 3337 // Create custom dropdown display 3338 const display = document.createElement('div'); 3339 display.className = 'oum-category-dropdown-display'; 3340 3341 // Create icon element 3342 const icon = document.createElement('img'); 3343 icon.className = 'oum-category-dropdown-icon'; 3344 3345 const linePreview = document.createElement('span'); 3346 linePreview.className = 'oum-category-line-preview'; 3347 3348 const areaPreview = document.createElement('span'); 3349 areaPreview.className = 'oum-category-area-preview'; 3350 3351 const routeIconTemplate = document.getElementById('oum-route-category-icon-template'); 3352 const areaIconTemplate = document.getElementById('oum-area-category-icon-template'); 3353 if (routeIconTemplate && routeIconTemplate.content) { 3354 linePreview.appendChild(routeIconTemplate.content.cloneNode(true)); 3355 } 3356 if (areaIconTemplate && areaIconTemplate.content) { 3357 areaPreview.appendChild(areaIconTemplate.content.cloneNode(true)); 3358 } 3359 3360 // Create text element 3361 const text = document.createElement('span'); 3362 text.className = 'oum-category-dropdown-text'; 3363 3364 display.appendChild(icon); 3365 display.appendChild(linePreview); 3366 display.appendChild(areaPreview); 3367 display.appendChild(text); 3368 3369 // Insert display before select 3370 wrapper.insertBefore(display, categorySelect); 3371 3372 // Make select invisible but still functional 3373 categorySelect.style.cssText = ` 3374 position: absolute; 3375 top: 0; 3376 left: 0; 3377 width: 100%; 3378 height: 100%; 3379 opacity: 0; 3380 cursor: pointer; 3381 z-index: 1; 3382 `; 3383 3384 // Update display when selection changes 3385 function updateDisplay() { 3386 const selectedOption = categorySelect.options[categorySelect.selectedIndex]; 3387 if (selectedOption && selectedOption.value) { 3388 const iconUrl = selectedOption.getAttribute('data-icon'); 3389 const categoryColor = selectedOption.getAttribute('data-color') || '#e82c71'; 3390 const categoryType = selectedOption.getAttribute('data-category-type') || 'point'; 3391 3392 if (categoryType === 'point' && iconUrl) { 3393 icon.src = iconUrl; 3394 icon.style.display = 'block'; 3395 } else { 3396 icon.style.display = 'none'; 3397 } 3398 3399 linePreview.style.color = categoryColor; 3400 linePreview.style.display = categoryType === 'polyline' ? 'inline-block' : 'none'; 3401 areaPreview.style.color = categoryColor; 3402 areaPreview.style.display = categoryType === 'polygon' ? 'inline-block' : 'none'; 3403 text.textContent = selectedOption.textContent; 3404 } else { 3405 icon.style.display = 'none'; 3406 linePreview.style.color = ''; 3407 linePreview.style.display = 'none'; 3408 areaPreview.style.color = ''; 3409 areaPreview.style.display = 'none'; 3410 text.textContent = 'Select a category...'; 3411 } 3412 } 3413 3414 // Initial display update 3415 updateDisplay(); 3416 if (typeof OUMFormMap !== "undefined" && OUMFormMap.updateLocationTypeCategoryUi) { 3417 OUMFormMap.updateLocationTypeCategoryUi(); 3418 updateDisplay(); 3419 } 3420 3421 // Handle selection change 3422 categorySelect.addEventListener('change', updateDisplay); 3423 3424 3425 // Make display focusable for accessibility 3426 display.setAttribute('tabindex', '0'); 3427 3428 // Handle keyboard navigation 3429 display.addEventListener('keydown', function(e) { 3430 if (e.key === 'Enter' || e.key === ' ') { 3431 e.preventDefault(); 3432 categorySelect.focus(); 3433 categorySelect.click(); 3434 } 3435 }); 3436 } 3437 } 3438 3439 function populateForm(location) { 3440 isEditMode = true; 3441 currentLocationId = location.post_id; 3442 3443 // Add edit-location class 3444 const addLocationEl = document.querySelector(".add-location"); 3445 if (addLocationEl) { 3446 addLocationEl.classList.add("edit-location"); 3447 } 3448 3449 // Set post_id 3450 const postIdField = document.getElementById("oum_post_id"); 3451 if (postIdField) { 3452 postIdField.value = location.post_id; 3453 } 3454 3455 // Basic fields 3456 const titleField = document.getElementById("oum_location_title"); 3457 const latField = document.getElementById("oum_location_lat"); 3458 const lngField = document.getElementById("oum_location_lng"); 3459 const addressField = document.getElementById("oum_location_address"); 3460 3461 if (titleField) titleField.value = location.title || ""; 3462 if (latField) latField.value = location.lat || ""; 3463 if (lngField) lngField.value = location.lng || ""; 3464 if (addressField) addressField.value = location.address || ""; 3465 3466 const geometryType = location.geometry_type === "polyline" || location.geometry_type === "polygon" ? location.geometry_type : "point"; 3467 OUMFormMap.setGeometryType(geometryType, { autoStartDrawing: false }); 3468 3469 // Marker types/categories 3470 if (location.types && Array.isArray(location.types)) { 3471 if (typeof oum_enable_multiple_marker_types !== 'undefined' && oum_enable_multiple_marker_types == 'true') { 3472 // Handle multiple marker types (checkboxes) 3473 const checkboxes = document.querySelectorAll('input[name="oum_marker_icon[]"]'); 3474 if (checkboxes.length > 0) {
3475 checkboxes.forEach(checkbox => { 3476 checkbox.checked = location.types.includes(checkbox.value); 3477 }); 3478 } 3479 } else { 3480 // Handle single marker type (select) 3481 const markerSelect = document.querySelector('select#oum_marker_icon'); 3482 if (markerSelect) { 3483 markerSelect.value = location.types[0] || ''; 3484 3485 // Update custom dropdown display if it exists 3486 const customDisplay = document.querySelector('.oum-category-dropdown-display'); 3487 if (customDisplay) { 3488 // Trigger the change event to update the display 3489 markerSelect.dispatchEvent(new Event('change')); 3490 } 3491 } 3492 } 3493 } 3494 3495 // Description 3496 const descriptionField = document.getElementById("oum_location_text"); 3497 if (descriptionField) { 3498 descriptionField.value = location.text || ""; 3499 } 3500 3501 // Video field 3502 const videoField = document.getElementById("oum_location_video"); 3503 if (videoField && location.video) { 3504 videoField.value = location.video; 3505 } 3506 3507 // Handle custom fields 3508 if (location.custom_fields && Array.isArray(location.custom_fields)) { 3509 location.custom_fields.forEach(field => { 3510 if (!field || typeof field.index === 'undefined') return; 3511 3512 if (field.fieldtype === 'checkbox') { 3513 const fieldValues = Array.isArray(field.val) ? field.val : [field.val]; 3514 const checkboxes = document.querySelectorAll( 3515 `input[type="checkbox"][name="oum_location_custom_fields[${field.index}][]"]` 3516 ); 3517 3518 if (checkboxes.length > 0) { 3519 checkboxes.forEach(checkbox => { 3520 if (checkbox) { 3521 const checkboxValue = checkbox.value.trim(); 3522 const normalizedValues = fieldValues.map(val => (val || '').toString().trim()); 3523 checkbox.checked = normalizedValues.includes(checkboxValue); 3524 } 3525 }); 3526 } 3527 } else if (field.fieldtype === 'radio') { 3528 const radioInputs = document.querySelectorAll( 3529 `input[type="radio"][name="oum_location_custom_fields[${field.index}]"]` 3530 ); 3531 3532 if (radioInputs.length > 0) { 3533 radioInputs.forEach(radio => { 3534 if (radio) { 3535 radio.checked = radio.value === field.val; 3536 } 3537 }); 3538 } 3539 } else if (field.fieldtype === 'select') { 3540 // Handle single and multiple select 3541 let input = document.querySelector(`[name="oum_location_custom_fields[${field.index}][]"]`); 3542 const isMultiple = !!input; 3543 if (!input) { 3544 input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`); 3545 } 3546 if (input && input.tagName === 'SELECT') { 3547 const values = Array.isArray(field.val) ? field.val.map(v => (v || '').toString().trim()) : [(field.val || '').toString().trim()]; 3548 Array.from(input.options).forEach(opt => { 3549 opt.selected = values.includes((opt.value || '').toString().trim()); 3550 }); 3551 } 3552 } else if (field.fieldtype === 'opening_hours') { 3553 // Handle opening hours field - expect JSON string 3554 try { 3555 const openingHoursData = JSON.parse(field.val); 3556 if (openingHoursData && typeof openingHoursData === 'object') { 3557 // Find the hours input 3558 const hoursInput = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`); 3559 const fieldContainer = document.querySelector(`[data-custom-field-index="${field.index}"]`); 3560 3561 // Check if 12-hour format is enabled for this field 3562 const use12hour = fieldContainer && fieldContainer.dataset.use12hour === '1'; 3563
3564 if (hoursInput && typeof OUMOpeningHours !== 'undefined') { 3565 // Convert JSON back to input format (with 12-hour format if enabled) 3566 hoursInput.value = OUMOpeningHours.formatForInput(openingHoursData, use12hour); 3567 } 3568 } 3569 } catch (e) { 3570 // Invalid JSON, try to set as raw value 3571 const input = document.querySelector(`input[name="oum_location_custom_fields[${field.index}][hours]"]`); 3572 if (input) { 3573 input.value = field.val || ''; 3574 } 3575 } 3576 } else { 3577 const input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`); 3578 if (input) { 3579 input.value = field.val || ''; 3580 } 3581 } 3582 }); 3583 } 3584 3585 // Handle images 3586 if (location.image) { 3587 const imageUrls = location.image.split('|').filter(url => url); 3588 const previewContainer = document.getElementById('oum_location_images_preview'); 3589 3590 // Remove required attribute from image upload field when editing with existing images 3591 const imageInput = document.getElementById('oum_location_images'); 3592 if (imageInput && imageUrls.length > 0) { 3593 imageInput.removeAttribute('required'); 3594 } 3595 3596 if (previewContainer) { 3597 previewContainer.innerHTML = ''; 3598 3599 imageUrls.forEach(url => { 3600 if (!url) return; 3601 3602 const previewItem = OUMMedia.createExistingImagePreview(url); 3603 if (!previewItem) { 3604 return; 3605 } 3606 3607 previewItem.style.opacity = "0"; 3608 previewItem.style.transform = "scale(0.9)"; 3609 previewItem.style.transition = "all 0.3s ease"; 3610 previewItem.style.opacity = "1"; 3611 previewItem.style.transform = "scale(1)"; 3612 previewContainer.appendChild(previewItem); 3613 }); 3614 } 3615 } 3616 3617 // Handle audio 3618 if (location.audio) { 3619 OUMMedia.setExistingAudio(location.audio); 3620 document.getElementById("oum_remove_existing_audio").value = "0"; 3621 } 3622 3623 // Set map view to location 3624 if (geometryType !== "point" && location.geometry) { 3625 OUMFormMap.setPolylineGeometry(location.geometry); 3626 OUMFormMap.startEditLayersTool(); 3627 } else if (location.lat && location.lng) { 3628 const locationZoom = location.zoom ? Number(location.zoom) : 12; 3629 OUMFormMap.setView(location.lat, location.lng, locationZoom); 3630 OUMFormMap.setLocation(location.lat, location.lng); 3631 } 3632 3633 // Sync checkbox validation after form population 3634 OUMCheckboxValidation.syncAllGroups(); 3635 } 3636 3637 function resetForm() { 3638 isEditMode = false; 3639 currentLocationId = null; 3640 selectedFiles = []; 3641 window.oumSelectedFiles = []; 3642 3643 const addLocationEl = document.querySelector(".add-location"); 3644 if (addLocationEl) { 3645 addLocationEl.classList.remove("edit-location"); 3646 } 3647 3648 // Reset form and message system 3649 const form = document.getElementById("oum_add_location"); 3650 const errorDiv = document.getElementById("oum_add_location_error"); 3651 const thankyouDiv = document.getElementById("oum_add_location_thankyou"); 3652 3653 // Reset form if it exists 3654 if (form) { 3655 form.reset(); 3656 form.style.display = 'block'; 3657 } 3658 3659 if (typeof OUMFormMap !== "undefined") { 3660 const typeField = document.getElementById("oum_geometry_type"); 3661 OUMFormMap.setGeometryType(typeField && typeField.value ? typeField.value : "point", { autoStartDrawing: false }); 3662 OUMFormMap.clearPolyline(); 3663 } 3664 3665 // Stop locate process 3666 if (window.map_locate_process) { 3667 window.map_locate_process.stop(); 3668 } 3669 3670 // Reset custom fields 3671 const customFields = document.querySelectorAll('[name^="oum_location_custom_fields"]'); 3672 if (customFields) { 3673 customFields.forEach(field => { 3674 if (field.type === 'checkbox' || field.type === 'radio') { 3675 field.checked = false; 3676 } else if (field.type === 'select-one') { 3677 field.selectedIndex = 0; 3678 } else { 3679 field.value = ''; 3680 } 3681 }); 3682 } 3683 3684 // Reset post_id field 3685 const postIdField = document.getElementById("oum_post_id"); 3686 if (postIdField) { 3687 postIdField.value = ""; 3688 } 3689 3690 // Reset oum_delete_location field 3691 const deleteLocationField = document.getElementById("oum_delete_location"); 3692 if (deleteLocationField) { 3693 deleteLocationField.value = ""; 3694 } 3695 3696 // Reset error message if it exists 3697 if (errorDiv) { 3698 errorDiv.style.display = 'none'; 3699 } 3700 3701 // Reset thank you message if it exists 3702 if (thankyouDiv) { 3703 thankyouDiv.style.display = 'none'; 3704 thankyouDiv.classList.remove('oum-delete-confirmation'); 3705 } 3706 3707 // Reset image preview 3708 const previewContainer = document.getElementById("oum_location_images_preview"); 3709 if (previewContainer) { 3710 previewContainer.innerHTML = ""; 3711 } 3712 3713 // Reset audio preview 3714 const audioInput = document.getElementById("oum_location_audio"); 3715 if (audioInput) { 3716 const previewContainer = audioInput.nextElementSibling; 3717 const previewDiv = previewContainer.querySelector('.audio-preview'); 3718 3719 // Clear the file input 3720 audioInput.value = ""; 3721 3722 // Clear the preview 3723 if (previewDiv) { 3724 previewDiv.innerHTML = ''; 3725 } 3726 3727 // Remove active state 3728 previewContainer.classList.remove("active"); 3729 } 3730 3731 // Reset hidden fields 3732 const removeExistingImage = document.getElementById("oum_remove_existing_image"); 3733 if (removeExistingImage) { 3734 removeExistingImage.value = "0"; 3735 } 3736 3737 const removeExistingAudio = document.getElementById("oum_remove_existing_audio"); 3738 if (removeExistingAudio) { 3739 removeExistingAudio.value = "0"; 3740 } 3741 3742 // Reset author section 3743 const authorSection = document.getElementById("oum_author"); 3744 if (authorSection) { 3745 authorSection.classList.remove("active"); 3746 } 3747 3748 // Reset name and email fields 3749 const nameField = document.getElementById("oum_location_author_name"); 3750 const emailField = document.getElementById("oum_location_author_email"); 3751 if (nameField && emailField) { 3752 nameField.required = false;
3753 emailField.required = false; 3754 } 3755 } 3756 3757 // Public interface 3758 return { 3759 init: function() { 3760 setupFormEvents(); 3761 setupDeleteButton(); // Add delete button handler 3762 setupCategoryDropdownIcons(); 3763 }, 3764 showFormMessage: showFormMessage, 3765 openForm: openForm, 3766 closeForm: closeForm, 3767 resetForm: resetForm, 3768 populateForm: populateForm, 3769 isEditMode: function() { 3770 return isEditMode; 3771 }, 3772 getCurrentLocationId: function() { 3773 return currentLocationId; 3774 } 3775 }; 3776})(); 3777 3778// OUMOpeningHours module is now in a separate file: frontend-opening-hours.js 3779// It's loaded as a dependency of this script 3780 3781/** 3782 * Media Module - Handles image upload and preview functionality 3783 */ 3784const OUMMedia = (function () { 3785 // Private variables 3786 let selectedFiles = []; 3787 let startX, startY, originalPosition, placeholder; 3788 let isDragging = false; 3789 3790 // Private functions 3791 function initializeImageUpload(imageInput) { 3792 if (!imageInput) return; 3793 3794 // Add click handler for upload icon label 3795 const uploadLabel = imageInput.parentElement.querySelector( 3796 'label[for="oum_location_images"]' 3797 ); 3798 if (uploadLabel) { 3799 uploadLabel.addEventListener("click", function (e) { 3800 e.preventDefault(); 3801 imageInput.click(); 3802 }); 3803 } 3804 3805 // Setup drag and drop handlers 3806 document.addEventListener("mousemove", handleDragMove); 3807 document.addEventListener("mouseup", handleDragEnd); 3808 if (imageInput.parentElement.querySelector(".image-preview-container")) { 3809 imageInput.parentElement.querySelector(".image-preview-container").addEventListener("dragover", handleDragOver); 3810 } 3811 3812 // Setup image input change handler 3813 imageInput.addEventListener("change", handleImageInputChange); 3814 } 3815 3816 function initializeAudioUpload() { 3817 const audioInput = document.getElementById('oum_location_audio'); 3818 if (!audioInput) return; 3819 3820 // Add click handler for upload icon label 3821 const uploadLabel = audioInput.parentElement.querySelector( 3822 'label[for="oum_location_audio"]' 3823 ); 3824 if (uploadLabel) { 3825 uploadLabel.addEventListener("click", function (e) { 3826 e.preventDefault(); 3827 audioInput.click(); 3828 }); 3829 } 3830 3831 // Setup audio input change handler 3832 audioInput.addEventListener("change", handleAudioInputChange); 3833 } 3834 3835 function handleAudioInputChange(e) { 3836 const audioInput = e.target; 3837 const previewContainer = audioInput.nextElementSibling; 3838 const previewDiv = previewContainer.querySelector('.audio-preview'); 3839 3840 if (audioInput.files && audioInput.files[0]) { 3841 const file = audioInput.files[0]; 3842 3843 previewContainer.classList.add('active'); 3844 3845 // Create audio preview element 3846 const audio = document.createElement('audio'); 3847 audio.controls = true; 3848 audio.style.width = '100%'; 3849 3850 const source = document.createElement('source'); 3851 source.src = URL.createObjectURL(file); 3852 source.type = file.type; 3853 3854 audio.appendChild(source); 3855 3856 // Replace existing audio preview if any 3857 previewDiv.innerHTML = ''; 3858 previewDiv.appendChild(audio); 3859 } 3860 } 3861 3862 function setExistingAudio(audioUrl) { 3863 if (!audioUrl) return; 3864 3865 const audioInput = document.getElementById('oum_location_audio'); 3866 if (!audioInput) return; 3867 3868 const previewContainer = audioInput.nextElementSibling; 3869 const previewDiv = previewContainer.querySelector('.audio-preview'); 3870 3871 previewContainer.classList.add('active'); 3872 3873 // Create audio preview element 3874 const audio = document.createElement('audio'); 3875 audio.controls = true; 3876 audio.style.width = '100%'; 3877 3878 const source = document.createElement('source'); 3879 source.src = audioUrl; 3880 source.type = 'audio/' + audioUrl.split('.').pop(); 3881 3882 audio.appendChild(source); 3883 3884 // Replace existing audio preview if any 3885 previewDiv.innerHTML = ''; 3886 previewDiv.appendChild(audio); 3887 } 3888 3889 function handleImageInputChange(e) { 3890 const imageInput = e.target; 3891 const previewContainer = document.getElementById( 3892 "oum_location_images_preview" 3893 ); 3894 const maxFiles = parseInt(imageInput.dataset.maxFiles) || 5; 3895 const maxFileSize = OUMConfig.defaults.media.maxImageSize; // in bytes 3896 3897 // Convert FileList to Array and store in a variable 3898 const files = Array.prototype.slice.call(e.target.files); 3899 const existingCount = selectedFiles.length; 3900 const totalFiles = existingCount + files.length; 3901 3902 if (totalFiles > maxFiles) { 3903 alert( 3904 OUMUtils.sprintf( 3905 oum_custom_strings.max_files_exceeded, 3906 maxFiles, 3907 maxFiles - existingCount 3908 ) 3909 ); 3910 } 3911 3912 // Process only up to remaining slots 3913 const remainingSlots = maxFiles - existingCount; 3914 const filesToProcess = files.slice(0, remainingSlots); 3915 3916 // Validate file sizes and collect valid files 3917 const validFiles = []; 3918 const invalidFiles = []; 3919 3920 filesToProcess.forEach(file => { 3921 if (file.size > maxFileSize) { 3922 invalidFiles.push(file.name); 3923 } else { 3924 validFiles.push(file); 3925 } 3926 }); 3927 3928 // Show error message for invalid files 3929 if (invalidFiles.length > 0) { 3930 const maxSizeMB = Math.round(maxFileSize / (1024 * 1024)); 3931 alert( 3932 OUMUtils.sprintf( 3933 oum_custom_strings.max_filesize_exceeded, 3934 maxSizeMB, 3935 invalidFiles.join('\n') 3936 ) 3937 ); 3938 } 3939 3940 // Update selected files with only valid ones 3941 selectedFiles = [...selectedFiles, ...validFiles]; 3942 3943 // Create previews for valid files only
3944 createImagePreviews(validFiles, previewContainer); 3945 3946 // Make selectedFiles available globally for the form submission 3947 window.oumSelectedFiles = selectedFiles; 3948 } 3949 3950 function createImagePreviews(files, container) { 3951 files.forEach((file) => { 3952 const reader = new FileReader(); 3953 3954 reader.onload = function (e) { 3955 const previewItem = createPreviewItem(e.target.result, file.name); 3956 3957 // Add the item with a fade-in animation 3958 previewItem.style.opacity = "0"; 3959 previewItem.style.transform = "scale(0.9)"; 3960 container.appendChild(previewItem); 3961 3962 // Trigger animation after a brief delay 3963 setTimeout(() => { 3964 previewItem.style.transition = "all 0.3s ease"; 3965 previewItem.style.opacity = "1"; 3966 previewItem.style.transform = "scale(1)"; 3967 }, 50); 3968 }; 3969 3970 reader.readAsDataURL(file); 3971 }); 3972 } 3973 3974 function createPreviewItem(imgSrc, fileName, isExisting) { 3975 const previewItem = document.createElement("div"); 3976 previewItem.className = isExisting ? "image-preview-item existing-image" : "image-preview-item"; 3977 if (fileName) { 3978 previewItem.dataset.fileName = fileName; 3979 } 3980 if (imgSrc) { 3981 previewItem.dataset.url = imgSrc; 3982 } 3983 3984 const img = document.createElement("img"); 3985 img.setAttribute("alt", "Preview"); 3986 img.src = imgSrc; 3987 3988 const remove = document.createElement("div"); 3989 remove.className = "remove-image"; 3990 remove.setAttribute("title", "Remove image"); 3991 remove.textContent = "\u00d7"; 3992 3993 const handle = document.createElement("div"); 3994 handle.className = "drag-handle"; 3995 handle.setAttribute("title", "Drag to reorder"); 3996 handle.textContent = "\u22ee\u22ee"; 3997 3998 previewItem.appendChild(img); 3999 previewItem.appendChild(remove); 4000 previewItem.appendChild(handle); 4001 4002 if (isExisting) { 4003 const hidden = document.createElement("input"); 4004 hidden.type = "hidden"; 4005 hidden.name = "existing_images[]"; 4006 hidden.value = imgSrc; 4007 previewItem.appendChild(hidden); 4008 } 4009 4010 // Add event listener for remove button 4011 const removeButton = previewItem.querySelector('.remove-image'); 4012 if (removeButton) { 4013 removeButton.addEventListener('click', handleRemoveImage); 4014 } 4015 4016 // Set up drag and drop for existing images 4017 setupDragAndDrop(previewItem); 4018 4019 return previewItem; 4020 } 4021 4022 function handleRemoveImage(e) { 4023 e.preventDefault(); 4024 const previewItem = this.closest(".image-preview-item"); 4025 4026 // If it's an existing image, handle removal differently 4027 if (previewItem.classList.contains("existing-image")) { 4028 const imgUrl = previewItem.querySelector("[name='existing_images[]']").value; 4029 const removedImagesInput = document.getElementById("oum_remove_existing_image"); 4030 const currentValue = removedImagesInput.value === "0" ? [] : removedImagesInput.value.split('|'); 4031 currentValue.push(imgUrl); 4032 removedImagesInput.value = currentValue.join('|'); 4033 } else { 4034 // Remove from selectedFiles array if it's a new image 4035 const fileName = previewItem.dataset.fileName; 4036 selectedFiles = selectedFiles.filter(file => file.name !== fileName); 4037 window.oumSelectedFiles = selectedFiles; 4038 } 4039 4040 // Animate and remove the preview item 4041 previewItem.style.transition = "all 0.3s ease"; 4042 previewItem.style.transform = "scale(0.8)"; 4043 previewItem.style.opacity = "0"; 4044 4045 setTimeout(() => { 4046 previewItem.remove(); 4047 }, 300); 4048 } 4049 4050 function setupDragAndDrop(previewItem) { 4051 previewItem.setAttribute('draggable', 'true'); 4052 4053 previewItem.addEventListener('mousedown', function(e) { 4054 if (e.target.classList.contains('remove-image')) return; 4055 4056 isDragging = true; 4057 this.classList.add('dragging'); 4058 4059 // Get element dimensions once at start 4060 const rect = this.getBoundingClientRect(); 4061 this.style.width = rect.width + 'px'; 4062 this.style.height = rect.height + 'px'; 4063 4064 // Store initial grid container for safety check 4065 this.initialContainer = this.closest('.oum-image-preview-grid'); 4066 4067 // Create placeholder immediately 4068 createPlaceholder(this); 4069 4070 // Set up dragged element 4071 this.style.position = 'fixed'; 4072 this.style.zIndex = '1000'; 4073 this.style.opacity = '0.9'; 4074 this.style.transform = 'scale(1.05) rotate(1deg)'; 4075 this.style.pointerEvents = 'none'; 4076 this.style.boxShadow = '0 5px 15px rgba(0,0,0,0.15)'; 4077 4078 // Set initial position 4079 moveDraggedElement(this, e); 4080 4081 document.body.style.cursor = 'grabbing'; 4082 }); 4083 4084 previewItem.addEventListener('touchstart', handleTouchStart); 4085 } 4086 4087 function createPlaceholder(element) { 4088 placeholder = document.createElement("div"); 4089 placeholder.classList.add("image-preview-placeholder"); 4090 placeholder.style.width = element.offsetWidth + "px"; 4091 placeholder.style.height = element.offsetHeight + "px"; 4092 placeholder.style.transition = "transform 0.2s ease"; 4093 placeholder.style.border = "2px dashed #e82c71"; 4094 placeholder.style.borderRadius = "4px"; 4095 placeholder.style.backgroundColor = "rgba(224, 42, 175, 0.05)"; 4096 element.parentNode.insertBefore(placeholder, element); 4097 } 4098 4099 function moveDraggedElement(draggable, e) { 4100 const rect = draggable.getBoundingClientRect(); 4101 const centerOffsetX = rect.width / 2; 4102 const centerOffsetY = rect.height / 2; 4103 4104 // Position element directly at cursor with center offset 4105 draggable.style.left = (e.clientX - centerOffsetX) + 'px'; 4106 draggable.style.top = (e.clientY - centerOffsetY) + 'px'; 4107 } 4108 4109 function updatePlaceholderPosition(e) { 4110 const previewContainer = document.getElementById("oum_location_images_preview"); 4111 if (!previewContainer) return; 4112 4113 const draggable = document.querySelector('.dragging'); 4114 if (!draggable) return; 4115 4116 const siblings = [...previewContainer.querySelectorAll(".image-preview-item:not(.dragging)")]; 4117 4118 // Find the closest sibling based on mouse position 4119 const closestSibling = siblings.reduce((closest, child) => { 4120 const rect = child.getBoundingClientRect(); 4121 const centerX = rect.left + rect.width / 2; 4122 const offset = e.clientX - centerX; 4123
4124 if (offset < 0 && (!closest.element || offset > closest.offset)) { 4125 return { offset: offset, element: child }; 4126 } 4127 return closest; 4128 }, { offset: Number.NEGATIVE_INFINITY, element: null }); 4129 4130 if (closestSibling.element) { 4131 previewContainer.insertBefore(placeholder, closestSibling.element); 4132 } else { 4133 previewContainer.appendChild(placeholder); 4134 } 4135 } 4136 4137 function handleDragMove(e) { 4138 if (!isDragging) return; 4139 4140 const draggable = document.querySelector(".dragging"); 4141 if (!draggable) return; 4142 4143 // Update dragged element position 4144 moveDraggedElement(draggable, e); 4145 4146 // Check if cursor is still within any grid container 4147 const gridContainer = document.getElementById("oum_location_images_preview"); 4148 if (!gridContainer) return; 4149 4150 const gridRect = gridContainer.getBoundingClientRect(); 4151 const isWithinGrid = e.clientX >= gridRect.left - 50 && 4152 e.clientX <= gridRect.right + 50 && 4153 e.clientY >= gridRect.top - 50 && 4154 e.clientY <= gridRect.bottom + 50; 4155 4156 // If cursor is outside grid boundaries, hide placeholder 4157 if (!isWithinGrid && placeholder) { 4158 placeholder.style.display = 'none'; 4159 } else if (placeholder) { 4160 placeholder.style.display = 'block'; 4161 updatePlaceholderPosition(e); 4162 } 4163 } 4164 4165 function handleDragEnd() { 4166 const draggable = document.querySelector(".dragging"); 4167 if (!draggable) return; 4168 4169 // Reset cursor 4170 document.body.style.cursor = ""; 4171 4172 // Check if we're still within the grid 4173 const gridContainer = document.getElementById("oum_location_images_preview"); 4174 if (!gridContainer) { 4175 // If no grid found, return item to its initial position 4176 if (draggable.initialContainer) { 4177 draggable.initialContainer.appendChild(draggable); 4178 } 4179 } else { 4180 // Place draggable element at placeholder position if within grid 4181 if (placeholder && placeholder.style.display !== 'none') { 4182 draggable.style.transition = "none"; 4183 placeholder.parentNode.insertBefore(draggable, placeholder); 4184 } else { 4185 // If placeholder is hidden (outside grid), append to end 4186 gridContainer.appendChild(draggable); 4187 } 4188 } 4189 4190 // Remove placeholder 4191 if (placeholder) { 4192 placeholder.remove(); 4193 } 4194 4195 // Reset draggable element styles 4196 draggable.style.position = ""; 4197 draggable.style.zIndex = ""; 4198 draggable.style.top = ""; 4199 draggable.style.left = ""; 4200 draggable.style.width = ""; 4201 draggable.style.height = ""; 4202 draggable.style.transform = ""; 4203 draggable.style.pointerEvents = ""; 4204 draggable.style.boxShadow = ""; 4205 draggable.classList.remove("dragging"); 4206 4207 isDragging = false; 4208 } 4209 4210 function handleTouchStart(e) { 4211 const touch = e.touches[0]; 4212 const mouseEvent = new MouseEvent("mousedown", { 4213 clientX: touch.clientX, 4214 clientY: touch.clientY 4215 }); 4216 this.dispatchEvent(mouseEvent); 4217 } 4218 4219 function handleDragOver(e) { 4220 e.preventDefault(); 4221 e.stopPropagation(); 4222 4223 const draggable = document.querySelector(".dragging"); 4224 if (!draggable) return; 4225 4226 const afterElement = getDragAfterElement(this, e.clientX); 4227 if (afterElement == null) { 4228 this.appendChild(placeholder); 4229 } else { 4230 this.insertBefore(placeholder, afterElement); 4231 } 4232 } 4233 4234 // Public interface 4235 return { 4236 init: function () { 4237 initializeAudioUpload(); 4238 }, 4239 initializeImageUpload: function(imageInput) { 4240 initializeImageUpload(imageInput); 4241 }, 4242 getSelectedFiles: function () { 4243 return selectedFiles; 4244 }, 4245 setupDragAndDrop: setupDragAndDrop, 4246 handleRemoveImage: handleRemoveImage, 4247 createExistingImagePreview: function(url) { 4248 if (!url) { 4249 return null; 4250 } 4251 return createPreviewItem(url, '', true); 4252 }, 4253 setExistingAudio: setExistingAudio 4254 }; 4255})(); 4256 4257/** 4258 * Checkbox Validation Module - Handles required checkbox group validation 4259 */ 4260const OUMCheckboxValidation = (function () { 4261 // Private functions 4262 function syncRequired(group) { 4263 const anyChecked = group.some(cb => cb.checked); 4264 const first = group[0]; 4265 first.required = !anyChecked; 4266 } 4267 4268 function getCheckboxGroups() { 4269 const checkboxGroups = document.querySelectorAll('.oum-checkbox-group.is-required input[type="checkbox"]'); 4270 4271 if (!checkboxGroups.length) return {}; 4272 4273 // Group checkboxes by name 4274 const groups = {}; 4275 checkboxGroups.forEach(checkbox => { 4276 const name = checkbox.name; 4277 if (!groups[name]) groups[name] = []; 4278 groups[name].push(checkbox); 4279 }); 4280 4281 return groups; 4282 } 4283 4284 function processCheckboxGroup(group, setupEvents = true) { 4285 const first = group[0]; 4286 4287 // Only process if first checkbox is required 4288 if (!first.required) return; 4289 4290 // Add event listeners if requested 4291 if (setupEvents) { 4292 group.forEach(cb => cb.addEventListener('change', () => syncRequired(group))); 4293 } 4294 4295 // Sync the state 4296 syncRequired(group); 4297 } 4298 4299 function initCheckboxGroups() { 4300 const groups = getCheckboxGroups(); 4301 4302 // Process each group with event listeners
4303 Object.keys(groups).forEach(groupName => { 4304 processCheckboxGroup(groups[groupName], true); 4305 }); 4306 } 4307 4308 // Public interface 4309 return { 4310 init: function() { 4311 initCheckboxGroups(); 4312 }, 4313 syncAllGroups: function() { 4314 const groups = getCheckboxGroups(); 4315 4316 // Sync each group without setting up new event listeners 4317 Object.keys(groups).forEach(groupName => { 4318 processCheckboxGroup(groups[groupName], false); 4319 }); 4320 } 4321 }; 4322})(); 4323 4324/** 4325 * Advanced Filter Interface Module - Handles advanced filtering functionality 4326 */ 4327const OUMAdvancedFilter = (function () { 4328 // Private variables 4329 let isInitialized = false; 4330 let filterSidebar = null; 4331 4332 // Private functions 4333 function init() { 4334 if (isInitialized) return; 4335 4336 filterSidebar = document.querySelector('.oum-advanced-filter-sidebar'); 4337 if (!filterSidebar) { 4338 return; 4339 } 4340 4341 setupFilterEvents(); 4342 setupFloatingPanelToggle(); 4343 isInitialized = true; 4344 } 4345 4346 // Setup click toggle for button/panel 4347 function setupFloatingPanelToggle() { 4348 const floatingPanel = document.querySelector('.oum-advanced-filter-button, .oum-advanced-filter-panel'); 4349 if (!floatingPanel || !floatingPanel.classList.contains('use-collapse')) { 4350 return; 4351 } 4352 4353 const toggle = floatingPanel.querySelector('.oum-advanced-filter-toggle'); 4354 const closeButton = floatingPanel.querySelector('.close-advanced-filter'); 4355 const filterWrapper = floatingPanel.closest('.oum-map-filter-wrapper'); 4356 4357 // Toggle active class on click 4358 if (toggle) { 4359 toggle.addEventListener('click', function(e) { 4360 e.stopPropagation(); 4361 floatingPanel.classList.toggle('active'); 4362 }); 4363 } 4364 4365 // Close button always collapses the floating panel 4366 if (closeButton) { 4367 closeButton.addEventListener('click', function(e) { 4368 e.stopPropagation(); 4369 floatingPanel.classList.remove('active'); 4370 }); 4371 } 4372 4373 // Close when clicking outside the filter wrapper 4374 if (filterWrapper) { 4375 document.addEventListener('click', function(e) { 4376 if (!filterWrapper.contains(e.target)) { 4377 floatingPanel.classList.remove('active'); 4378 } 4379 }); 4380 } 4381 } 4382 4383 function setupFilterEvents() { 4384 // Get all filter inputs 4385 const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input'); 4386 const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox'); 4387 const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio'); 4388 const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select'); 4389 4390 // Add event listeners to all filter inputs 4391 filterInputs.forEach(input => { 4392 input.addEventListener('input', handleFilterChange); 4393 }); 4394 4395 filterCheckboxes.forEach(checkbox => { 4396 checkbox.addEventListener('change', handleFilterChange); 4397 }); 4398 4399 filterRadios.forEach(radio => { 4400 radio.addEventListener('change', handleFilterChange); 4401 }); 4402 4403 filterSelects.forEach(select => { 4404 select.addEventListener('change', handleFilterChange); 4405 }); 4406 4407 // Add reset button functionality 4408 const resetButton = filterSidebar.querySelector('#oum-advanced-filter-reset'); 4409 if (resetButton) { 4410 resetButton.addEventListener('click', resetAllFilters); 4411 } 4412 } 4413 4414 function handleFilterChange() { 4415 if (typeof OUMMarkers === 'undefined' || !OUMMarkers.setCustomfieldsFilter) { 4416 console.warn('OUMMarkers not available for advanced filtering'); 4417 return; 4418 } 4419 4420 const filterCriteria = getFilterCriteria(); 4421 OUMMarkers.setCustomfieldsFilter(filterCriteria); 4422 } 4423 4424 function getFilterCriteria() { 4425 const criteria = {}; 4426 if (!filterSidebar) return criteria; 4427 4428 // Get text/email/url inputs 4429 const textInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input'); 4430 textInputs.forEach(input => { 4431 const customFieldId = input.dataset.customFieldId; 4432 if (customFieldId && input.value.trim()) { 4433 criteria[customFieldId] = { 4434 type: 'text', 4435 value: input.value.trim().toLowerCase() 4436 }; 4437 } 4438 }); 4439 4440 // Get checkbox selections 4441 const checkboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox');
4442 checkboxes.forEach(checkbox => { 4443 const customFieldId = checkbox.dataset.customFieldId; 4444 const filterType = checkbox.dataset.filterType; 4445 4446 if (customFieldId) { 4447 // Check if this is an opening_hours open_now filter 4448 if (filterType === 'opening_hours_open_now') { 4449 if (checkbox.checked) { 4450 criteria[customFieldId] = { 4451 type: 'opening_hours_open_now', 4452 value: 'open' 4453 }; 4454 } 4455 } else { 4456 // Standard checkbox handling 4457 if (!criteria[customFieldId]) { 4458 // Get checkbox relation from the fieldset (default to OR for backward compatibility) 4459 const fieldset = checkbox.closest('.oum-checkbox-group'); 4460 const relation = fieldset ? (fieldset.dataset.checkboxRelation || 'OR') : 'OR'; 4461 criteria[customFieldId] = { type: 'checkbox', values: [], relation: relation }; 4462 } 4463 if (checkbox.checked) { 4464 criteria[customFieldId].values.push(checkbox.value); 4465 } 4466 } 4467 } 4468 }); 4469 4470 // Get radio selections 4471 const radios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio'); 4472 radios.forEach(radio => { 4473 const customFieldId = radio.dataset.customFieldId; 4474 if (customFieldId && radio.checked) { 4475 criteria[customFieldId] = { 4476 type: 'radio', 4477 value: radio.value 4478 }; 4479 } 4480 }); 4481 4482 // Get select selections 4483 const selects = filterSidebar.querySelectorAll('.oum-advanced-filter-select'); 4484 selects.forEach(select => { 4485 const customFieldId = select.dataset.customFieldId; 4486 if (customFieldId && select.value) { 4487 criteria[customFieldId] = { 4488 type: 'select', 4489 value: select.value 4490 }; 4491 } 4492 }); 4493 4494 // Remove empty criteria (no checkboxes selected, empty text inputs, etc.) 4495 Object.keys(criteria).forEach(key => { 4496 const criterion = criteria[key]; 4497 if (criterion.type === 'checkbox' && criterion.values.length === 0) { 4498 delete criteria[key]; 4499 } 4500 }); 4501 4502 return criteria; 4503 } 4504 4505 function resetAllFilters() { 4506 // Clear all text inputs 4507 const filterInputs = filterSidebar.querySelectorAll('.oum-advanced-filter-input'); 4508 filterInputs.forEach(input => { 4509 input.value = ''; 4510 }); 4511 4512 // Uncheck all checkboxes 4513 const filterCheckboxes = filterSidebar.querySelectorAll('.oum-advanced-filter-checkbox'); 4514 filterCheckboxes.forEach(checkbox => { 4515 checkbox.checked = false; 4516 }); 4517 4518 // Uncheck all radio buttons 4519 const filterRadios = filterSidebar.querySelectorAll('.oum-advanced-filter-radio'); 4520 filterRadios.forEach(radio => { 4521 radio.checked = false; 4522 }); 4523 4524 // Reset all select dropdowns to first option 4525 const filterSelects = filterSidebar.querySelectorAll('.oum-advanced-filter-select'); 4526 filterSelects.forEach(select => { 4527 select.selectedIndex = 0; 4528 }); 4529 4530 if (typeof OUMMarkers !== 'undefined' && OUMMarkers.setCustomfieldsFilter) { 4531 OUMMarkers.setCustomfieldsFilter(null); 4532 } 4533 4534 // Restore Marker Categories to the default "all checked" state 4535 if (typeof OUMMarkers !== 'undefined' && OUMMarkers.resetCategoriesFilter) { 4536 OUMMarkers.resetCategoriesFilter(); 4537 } 4538 4539 resetSearchbar(); 4540 } 4541 4542 function resetSearchbar() { 4543 // Live Filter Markers 4544 const liveFilterInput = document.getElementById("oum_filter_markers"); 4545 if (liveFilterInput) { 4546 liveFilterInput.value = ""; 4547 if (typeof OUMMarkers !== "undefined" && OUMMarkers.updateSearchtextAndCategoriesFilters) { 4548 OUMMarkers.updateSearchtextAndCategoriesFilters(); 4549 } 4550 } 4551 4552 // Marker search (leaflet-search) 4553 const markerSearchCancel = document.querySelector("#oum_search_marker .search-cancel"); 4554 if (markerSearchCancel) { 4555 markerSearchCancel.click(); 4556 } else { 4557 const markerSearchInput = document.querySelector("#oum_search_marker .search-input"); 4558 if (markerSearchInput) { 4559 markerSearchInput.value = ""; 4560 markerSearchInput.dispatchEvent(new Event("input", { bubbles: true })); 4561 markerSearchInput.dispatchEvent(new Event("keyup", { bubbles: true })); 4562 } 4563 } 4564 4565 // Address search (GeoSearch) 4566 const addressReset = document.querySelector("#oum_search_address button.reset, .oum-searchbar-filter .leaflet-geosearch-bar button.reset"); 4567 if (addressReset) { 4568 addressReset.click(); 4569 } else { 4570 const addressInput = document.querySelector("#oum_search_address input, .oum-searchbar-filter .leaflet-geosearch-bar input"); 4571 if (addressInput) { 4572 addressInput.value = ""; 4573 } 4574 } 4575 } 4576 4577 // Public interface 4578 return { 4579 init: init 4580 }; 4581})(); 4582 4583// Main initialization function 4584function oumInitializeMap() { 4585 // Only proceed if we have a map element 4586 if (!document.getElementById(map_el)) { 4587 console.warn('â¼ï¸ Open User Map: Map container missing. Initialization aborted. Disable page caching if applicable, or contact [email protected] for help.'); 4588 return; 4589 } 4590 4591 // Initialize opening hours UI 4592 OUMOpeningHours.init(); 4593 4594 // Restore the extended L object 4595 window.L = window.OUMLeaflet.L; 4596 4597 // Initialize map and get instance 4598 const mapInstance = OUMMap.init(map_el); 4599 4600 // Initialize markers 4601 const markersModule = OUMMarkers.init(mapInstance); 4602 4603 // Add markers from the global oum_all_locations 4604 if ( 4605 typeof oum_all_locations !== "undefined" && 4606 Array.isArray(oum_all_locations) 4607 ) { 4608 markersModule.addMarkers(oum_all_locations); 4609 } 4610 4611 // Initialize location form 4612 OUMFormMap.init(mapInstance); 4613 4614 // Initialize form controller 4615 OUMFormController.init(); 4616 4617 // Initialize media handling 4618 OUMMedia.init(); 4619 4620 // Initialize checkbox validation 4621 OUMCheckboxValidation.init(); 4622 4623 // Initialize Advanced Filter Interface 4624 OUMAdvancedFilter.init(); 4625 4626 // Setup filter events 4627 const markerFilterInput = document.getElementById("oum_filter_markers"); 4628 if (markerFilterInput) { 4629 markerFilterInput.addEventListener("input", OUMMarkers.updateSearchtextAndCategoriesFilters); 4630 } 4631 4632 const categoryInputs = document.querySelectorAll(
4633 '.open-user-map .oum-marker-categories-filter [name="type"]' 4634 ); 4635 if (categoryInputs.length > 0) { 4636 categoryInputs.forEach((input) => { 4637 input.addEventListener("change", OUMMarkers.updateSearchtextAndCategoriesFilters); 4638 }); 4639 } 4640 4641 // Execute custom JS from settings 4642 if (typeof custom_js !== "undefined" && custom_js.snippet) { 4643 try { 4644 // Wrap custom JS execution in a try-catch with proper element existence checks 4645 const wrappedJS = ` 4646 try { 4647 if (typeof document !== 'undefined') { 4648 // Defer map2-related code execution 4649 if (${custom_js.snippet.includes("oumMap2")}) { 4650 // Create a MutationObserver to watch for the form map initialization 4651 const observer = new MutationObserver((mutations) => { 4652 if (window.oumMap2) { 4653 // Execute the custom JS only when oumMap2 is available 4654 try { 4655 ${custom_js.snippet} 4656 } catch (e) { 4657 console.warn('Custom JS execution error (deferred):', e); 4658 } 4659 observer.disconnect(); 4660 } 4661 }); 4662 4663 // Start observing the document for the form map to be added 4664 observer.observe(document.body, { 4665 childList: true, 4666 subtree: true 4667 }); 4668 } else { 4669 // Execute non-map2 related code immediately 4670 ${custom_js.snippet} 4671 } 4672 } 4673 } catch (e) { 4674 console.warn('Custom JS execution error:', e); 4675 } 4676 `; 4677 Function(wrappedJS)(); 4678 } catch (error) { 4679 console.warn("Error executing custom JS:", error); 4680 } 4681 } 4682}
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.