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 // Add zoom offset to match settings map view 59 zoom = zoom + 0.7; 60 61 // Validate coordinates 62 if (!validateCoordinates(lat, lng)) { 63 console.warn("Invalid coordinates for latLngToBounds, using defaults"); 64 return [ 65 [OUMConfig.defaults.map.lat, OUMConfig.defaults.map.lng], 66 [OUMConfig.defaults.map.lat, OUMConfig.defaults.map.lng], 67 ]; 68 } 69 70 // Validate dimensions 71 if (isNaN(width) || width <= 0 || isNaN(height) || height <= 0) { 72 console.warn("Invalid dimensions for latLngToBounds"); 73 width = 570; // Default width 74 height = 372; // Default height 75 } 76 77 // Validate and adjust zoom 78 if (isNaN(zoom)) { 79 zoom = OUMConfig.defaults.map.zoom; 80 } else { 81 // Ensure zoom is between 2 and 20 82 zoom = Math.max(2, Math.min(20, zoom)); 83 } 84 85 // Calculate the visible area based on zoom level and Mercator projection 86 const EARTH_RADIUS = 6378137; // Earth's radius in meters 87 const scale = Math.pow(2, zoom); 88 89 // Convert pixel dimensions to meters at this zoom level 90 const metersPerPixel = (2 * Math.PI * EARTH_RADIUS) / (256 * scale); 91 const widthMeters = width * metersPerPixel; 92 const heightMeters = height * metersPerPixel; 93 94 // Calculate the latitude bounds, accounting for Mercator projection 95 const latRad = (lat * Math.PI) / 180; 96 const latDelta = heightMeters / 2 / EARTH_RADIUS; 97 const latitudeNorth = ((latRad + latDelta) * 180) / Math.PI; 98 const latitudeSouth = ((latRad - latDelta) * 180) / Math.PI; 99 100 // Calculate the longitude bounds (simpler as it's linear in Mercator) 101 const lngDelta = widthMeters / 2 / (EARTH_RADIUS * Math.cos(latRad)); 102 const longitudeEast = lng + (lngDelta * 180) / Math.PI; 103 const longitudeWest = lng - (lngDelta * 180) / Math.PI; 104 105 return [ 106 [latitudeSouth, longitudeWest], 107 [latitudeNorth, longitudeEast], 108 ]; 109 } 110 111 function customAutoSuggestText(text, val) { 112 return ( 113 '<div><img src="' + 114 val.layer.options.icon.options.iconUrl + 115 '" />' + 116 val.layer.options.title + 117 "</div>" 118 ); 119 } 120 121 function initGeosearchProvider() { 122 let provider; 123 switch (oum_geosearch_provider) { 124 case "osm": 125 provider = new GeoSearch.OpenStreetMapProvider(); 126 break; 127 case "geoapify": 128 provider = new GeoSearch.GeoapifyProvider({ 129 params: { 130 apiKey: oum_geosearch_provider_geoapify_key, 131 }, 132 }); 133 break; 134 case "here": 135 provider = new GeoSearch.HereProvider({ 136 params: { 137 apiKey: oum_geosearch_provider_here_key, 138 }, 139 }); 140 break; 141 case "mapbox": 142 provider = new GeoSearch.MapBoxProvider({ 143 params: {
144 access_token: oum_geosearch_provider_mapbox_key, 145 }, 146 }); 147 break; 148 default: 149 provider = new GeoSearch.OpenStreetMapProvider(); 150 break; 151 } 152 return provider; 153 } 154 155 /** 156 * Validates and sanitizes coordinates 157 * @param {number|string} lat 158 * @param {number|string} lng 159 * @returns {boolean} 160 */ 161 function validateCoordinates(lat, lng) { 162 const parsedLat = parseFloat(lat); 163 const parsedLng = parseFloat(lng); 164 return ( 165 !isNaN(parsedLat) && 166 !isNaN(parsedLng) && 167 parsedLat >= -90 && 168 parsedLat <= 90 && 169 parsedLng >= -180 && 170 parsedLng <= 180 171 ); 172 } 173 174 /** 175 * Safely parses a JSON string 176 * @param {string} str 177 * @param {*} fallback 178 * @returns {*} 179 */ 180 function safeJSONParse(str, fallback = null) { 181 try { 182 return JSON.parse(str); 183 } catch (e) { 184 return fallback; 185 } 186 } 187 188 /** 189 * Debounces a function 190 * @param {Function} func 191 * @param {number} wait 192 * @returns {Function} 193 */ 194 function debounce(func, wait = 250) { 195 let timeout; 196 return function executedFunction(...args) { 197 const later = () => { 198 clearTimeout(timeout); 199 func(...args); 200 }; 201 clearTimeout(timeout); 202 timeout = setTimeout(later, wait); 203 }; 204 } 205 206 // Public interface 207 return { 208 getParameterByName, 209 latLngToBounds, 210 customAutoSuggestText, 211 initGeosearchProvider, 212 validateCoordinates, 213 safeJSONParse, 214 debounce, 215 }; 216})(); 217 218/** 219 * Error Handler Module - Centralizes error management 220 */ 221const OUMErrorHandler = (function () { 222 function showError(message, type = "error") { 223 console.error(`OUM Error: ${message}`); 224 225 // Show error in UI if error container exists 226 const errorContainer = document.getElementById("oum_add_location_error"); 227 if (errorContainer) { 228 errorContainer.innerHTML = `${message}<br>`; 229 errorContainer.style.display = "block"; 230 } 231 } 232 233 function handleAjaxError(error) { 234 showError(`Ajax request failed: ${error.message}`); 235 } 236 237 function validateCoordinates(lat, lng) { 238 const parsedLat = parseFloat(lat); 239 const parsedLng = parseFloat(lng); 240 241 if (isNaN(parsedLat) || isNaN(parsedLng)) { 242 showError("Invalid coordinates provided"); 243 return false; 244 } 245 246 if ( 247 parsedLat < -90 || 248 parsedLat > 90 || 249 parsedLng < -180 || 250 parsedLng > 180 251 ) { 252 showError("Coordinates out of valid range"); 253 return false; 254 } 255 256 return true; 257 } 258 259 return { 260 showError, 261 handleAjaxError, 262 validateCoordinates, 263 }; 264})(); 265 266/** 267 * Configuration Module - Centralizes all configuration settings 268 */ 269const OUMConfig = (function () { 270 // Private variables 271 const defaults = { 272 map: { 273 lat: 28, 274 lng: 0, 275 zoom: 1, 276 bounds: L.latLngBounds( 277 L.latLng(-85, -200), // Southwest corner (adjusted to prevent grey areas) 278 L.latLng(85, 200) // Northeast corner (adjusted to prevent grey areas) 279 ), 280 }, 281 media: { 282 maxFiles: 5, 283 validImageExtensions: ["jpeg", "jpg", "png", "webp"], 284 maxImageSize: (oum_max_image_filesize || 10) * 1048576, // Convert MB to bytes 285 }, 286 search: { 287 zoomLevel: oum_searchmarkers_zoom || 8, 288 addressLabel: oum_searchaddress_label || "Search for address", 289 markersLabel: oum_searchmarkers_label || "Find marker", 290 }, 291 }; 292 293 function getMapStyle() { 294 return window.mapStyle || "Esri.WorldStreetMap"; 295 } 296 297 function getTileProviderKey() { 298 return window.oum_tile_provider_mapbox_key || ""; 299 } 300 301 function getGeosearchProvider() { 302 let provider; 303 switch (oum_geosearch_provider) { 304 case "geoapify": 305 provider = new GeoSearch.GeoapifyProvider({ 306 params: { 307 apiKey: oum_geosearch_provider_geoapify_key, 308 }, 309 }); 310 break; 311 case "here": 312 provider = new GeoSearch.HereProvider({ 313 params: { 314 apiKey: oum_geosearch_provider_here_key, 315 }, 316 }); 317 break; 318 case "mapbox": 319 provider = new GeoSearch.MapBoxProvider({ 320 params: {
321 access_token: oum_geosearch_provider_mapbox_key, 322 }, 323 }); 324 break; 325 default: 326 provider = new GeoSearch.OpenStreetMapProvider(); 327 } 328 return provider; 329 } 330 331 return { 332 defaults, 333 getMapStyle, 334 getTileProviderKey, 335 getGeosearchProvider, 336 }; 337})(); 338 339/** 340 * Map Core Module - Handles the main map initialization and configuration 341 */ 342const OUMMap = (function () { 343 // Private variables 344 let map = null; 345 let world_bounds = null; 346 let startPosition = { 347 lat: 348 typeof start_lat !== "undefined" 349 ? Number(start_lat) 350 : OUMConfig.defaults.map.lat, 351 lng: 352 typeof start_lng !== "undefined" 353 ? Number(start_lng) 354 : OUMConfig.defaults.map.lng, 355 zoom: 356 typeof start_zoom !== "undefined" 357 ? Number(start_zoom) 358 : OUMConfig.defaults.map.zoom, 359 }; 360 361 // Private functions 362 function initializeStartPosition() { 363 // Validate coordinates 364 if (!OUMUtils.validateCoordinates(startPosition.lat, startPosition.lng)) { 365 console.warn("Invalid coordinates, using defaults"); 366 startPosition.lat = OUMConfig.defaults.map.lat; 367 startPosition.lng = OUMConfig.defaults.map.lng; 368 } 369 370 // Validate zoom level (between 1 and 20) 371 if ( 372 isNaN(startPosition.zoom) || 373 startPosition.zoom < 1 || 374 startPosition.zoom > 20 375 ) { 376 console.warn("Invalid zoom level, using default"); 377 startPosition.zoom = OUMConfig.defaults.map.zoom; 378 } 379 } 380 381 function setupMapBounds() { 382 // Set bounds if fixed map bounds is enabled 383 if (oum_enable_fixed_map_bounds) { 384 // Calculate bounds based on initial position 385 const boundsArray = OUMUtils.latLngToBounds( 386 startPosition.lat, 387 startPosition.lng, 388 startPosition.zoom, 389 570, // Width of settings map 390 372 // Height of settings map 391 ); 392 393 // Convert the bounds array to a Leaflet LatLngBounds object 394 world_bounds = L.latLngBounds( 395 L.latLng(boundsArray[0][0], boundsArray[0][1]), 396 L.latLng(boundsArray[1][0], boundsArray[1][1]) 397 ); 398 399 // Store bounds globally for form map to use 400 sharedMapBounds = world_bounds; 401 } else { 402 // Use default world bounds when fixed map bounds is disabled 403 world_bounds = OUMConfig.defaults.map.bounds; 404 sharedMapBounds = world_bounds; 405 } 406 407 // Set the minimum zoom level to prevent zooming out too far 408 const maxVisibleBounds = map.getBoundsZoom(world_bounds); 409 map.setMinZoom(maxVisibleBounds); 410 411 // Set max bounds without padding 412 map.setMaxBounds(world_bounds); 413 414 let isAdjusting = false; 415 416 // Handle map movement without recursion 417 map.on("moveend", function () { 418 if (isAdjusting) return; 419 isAdjusting = true; 420 421 const zoom = map.getZoom(); 422 423 // Only enforce bounds if we're zoomed in beyond the minimum zoom 424 if (zoom > maxVisibleBounds) { 425 const currentBounds = map.getBounds(); 426 const currentCenter = map.getCenter(); 427 428 let needsAdjustment = false; 429 let newLat = currentCenter.lat; 430 let newLng = currentCenter.lng; 431 432 // Calculate current viewport dimensions 433 const viewportHeight = 434 currentBounds.getNorth() - currentBounds.getSouth(); 435 const viewportWidth = 436 currentBounds.getEast() - currentBounds.getWest(); 437 438 // Check and adjust latitude (north/south) 439 if (currentBounds.getNorth() > world_bounds.getNorth()) { 440 newLat = world_bounds.getNorth() - viewportHeight / 2; 441 needsAdjustment = true; 442 } else if (currentBounds.getSouth() < world_bounds.getSouth()) { 443 newLat = world_bounds.getSouth() + viewportHeight / 2; 444 needsAdjustment = true; 445 } 446 447 // Check and adjust longitude (east/west) 448 if (currentBounds.getEast() > world_bounds.getEast()) { 449 newLng = world_bounds.getEast() - viewportWidth / 2; 450 needsAdjustment = true; 451 } else if (currentBounds.getWest() < world_bounds.getWest()) { 452 newLng = world_bounds.getWest() + viewportWidth / 2; 453 needsAdjustment = true; 454 } 455 456 if (needsAdjustment) {
457 map.setView([newLat, newLng], zoom, { animate: false }); 458 } 459 } 460 461 isAdjusting = false; 462 }); 463 } 464 465 function setupTileLayer(mapStyle) { 466 let tileLayer; 467 468 if (mapStyle == "Custom1") { 469 tileLayer = L.tileLayer( 470 "https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png" 471 ).addTo(map); 472 L.tileLayer( 473 "https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png", 474 { 475 tileSize: 512, 476 zoomOffset: -1, 477 } 478 ).addTo(map); 479 } else if (mapStyle == "Custom2") { 480 tileLayer = L.tileLayer( 481 "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png" 482 ).addTo(map); 483 L.tileLayer( 484 "https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png", 485 { 486 tileSize: 512, 487 zoomOffset: -1, 488 } 489 ).addTo(map); 490 } else if (mapStyle == "Custom3") { 491 tileLayer = L.tileLayer( 492 "https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png" 493 ).addTo(map); 494 L.tileLayer( 495 "https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png", 496 { 497 tileSize: 512, 498 zoomOffset: -1, 499 } 500 ).addTo(map); 501 } else if (mapStyle == "MapBox.streets") { 502 tileLayer = L.tileLayer 503 .provider("MapBox", { 504 id: "mapbox/streets-v12", 505 accessToken: OUMConfig.getTileProviderKey(), 506 }) 507 .addTo(map); 508 } else if (mapStyle == "MapBox.outdoors") { 509 tileLayer = L.tileLayer 510 .provider("MapBox", { 511 id: "mapbox/outdoors-v12", 512 accessToken: OUMConfig.getTileProviderKey(), 513 }) 514 .addTo(map); 515 } else if (mapStyle == "MapBox.light") { 516 tileLayer = L.tileLayer 517 .provider("MapBox", { 518 id: "mapbox/light-v11", 519 accessToken: OUMConfig.getTileProviderKey(), 520 }) 521 .addTo(map); 522 } else if (mapStyle == "MapBox.dark") { 523 tileLayer = L.tileLayer 524 .provider("MapBox", { 525 id: "mapbox/dark-v11", 526 accessToken: OUMConfig.getTileProviderKey(), 527 }) 528 .addTo(map); 529 } else if (mapStyle == "MapBox.satellite") { 530 tileLayer = L.tileLayer 531 .provider("MapBox", { 532 id: "mapbox/satellite-v9", 533 accessToken: OUMConfig.getTileProviderKey(), 534 }) 535 .addTo(map); 536 } else if (mapStyle == "MapBox.satellite-streets") { 537 tileLayer = L.tileLayer 538 .provider("MapBox", { 539 id: "mapbox/satellite-streets-v12", 540 accessToken: OUMConfig.getTileProviderKey(), 541 }) 542 .addTo(map); 543 } else { 544 // Default 545 tileLayer = L.tileLayer.provider(mapStyle).addTo(map); 546 } 547 548 return tileLayer; 549 } 550 551 function setupControls() { 552 // Search markers control as button 553 if (oum_enable_searchmarkers_button) { 554 L.control 555 .search({ 556 textPlaceholder: oum_searchmarkers_label, 557 layer: window.oumMarkersLayer, 558 propertyName: "content", 559 initial: false, 560 buildTip: OUMUtils.customAutoSuggestText, 561 firstTipSubmit: true, 562 autoCollapse: true, 563 zoom: oum_searchmarkers_zoom, 564 }) 565 .addTo(map); 566 } 567 568 // Search markers control as searchbar 569 if (oum_enable_searchbar && oum_searchbar_type === "markers") { 570 L.control 571 .search({ 572 textPlaceholder: oum_searchmarkers_label, 573 layer: window.oumMarkersLayer, 574 propertyName: "content", 575 initial: false, 576 buildTip: OUMUtils.customAutoSuggestText, 577 firstTipSubmit: true, 578 autoCollapse: false, 579 collapsed: false, 580 zoom: oum_searchmarkers_zoom, 581 container: 'oum_search_marker' 582 }) 583 .addTo(map); 584 } 585 586 // Add searchbar for address search 587 if (oum_enable_searchbar && oum_searchbar_type === "address") { 588 const searchBar = new GeoSearch.GeoSearchControl({ 589 style: "bar", 590 showMarker: false, 591 provider: OUMUtils.initGeosearchProvider(), 592 searchLabel: oum_searchaddress_label, 593 updateMap: false, 594 }); 595 map.addControl(searchBar); 596 } 597 598 // Add button for address search 599 if (oum_enable_searchaddress_button) { 600 const searchButton = new GeoSearch.GeoSearchControl({ 601 style: "button", 602 showMarker: false, 603 provider: OUMUtils.initGeosearchProvider(), 604 searchLabel: oum_searchaddress_label, 605 updateMap: false, 606 }); 607 map.addControl(searchButton); 608 } 609 610 // Current location control 611 if (oum_enable_currentlocation) { 612 window.map_locate_process = L.control 613 .locate({ 614 flyTo: true, 615 showPopup: false, 616 }) 617 .addTo(map); 618 } 619 } 620 621 function setupMapEvents() { 622 // Event: pan or zoom Map 623 map.on("moveend", function (ev) { 624 startPosition.lat = map.getCenter().lat; 625 startPosition.lng = map.getCenter().lng; 626 startPosition.zoom = map.getZoom(); 627 }); 628 629 // Event: Enter/Exit Fullscreen 630 const addLocationPopup = document.querySelector("#add-location-overlay"); 631 const originalContainer = addLocationPopup?.parentElement; 632 const fullscreenContainer = document.querySelector(
633 ".open-user-map .map-wrap" 634 ); 635 636 if (addLocationPopup) { 637 map.on("enterFullscreen", function () { 638 fullscreenContainer.appendChild(addLocationPopup); 639 }); 640 641 map.on("exitFullscreen", function () { 642 originalContainer.appendChild(addLocationPopup); 643 }); 644 } 645 646 // Event: geosearch success 647 map.on("geosearch/showlocation", function(e) { 648 let coords = e.marker._latlng; 649 let isInBounds = map.getBounds().contains(coords); 650 651 if (!isInBounds && oum_enable_fixed_map_bounds === 'on') { 652 console.log("This search result is out of reach."); 653 const searchBar = document.querySelector(`#${map_el} .leaflet-geosearch-bar form`); 654 if (searchBar) { 655 searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)"; 656 setTimeout(function () { 657 searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)"; 658 }, 2000); 659 } 660 } else { 661 // Handle valid search result 662 if (e.location.bounds) { 663 map.flyToBounds(e.location.bounds); 664 } else if (e.location.raw && e.location.raw.mapView) { 665 map.flyToBounds([ 666 [e.location.raw.mapView.south, e.location.raw.mapView.west], 667 [e.location.raw.mapView.north, e.location.raw.mapView.east] 668 ]); 669 } else { 670 map.flyTo([e.location.y, e.location.x], 17); 671 } 672 } 673 }); 674 } 675 676 function setupRegionEvents() { 677 document 678 .querySelectorAll(".open-user-map .change_region") 679 .forEach(function (btn) { 680 btn.onclick = function (event) { 681 let el = event.currentTarget; 682 let region_lat = el.getAttribute("data-lat"); 683 let region_lng = el.getAttribute("data-lng"); 684 let region_zoom = el.getAttribute("data-zoom"); 685 686 let region_bounds = OUMUtils.latLngToBounds( 687 parseFloat(region_lat), 688 parseFloat(region_lng), 689 parseFloat(region_zoom), 690 570, 691 372 692 ); 693 let region_bounds_zoom = map.getBoundsZoom(region_bounds); 694 695 // Center Map 696 map.flyTo([region_lat, region_lng], region_bounds_zoom); 697 698 // Update active state 699 document 700 .querySelectorAll(".open-user-map .change_region") 701 .forEach(function (el) { 702 el.classList.remove("active"); 703 }); 704 el.classList.add("active"); 705 }; 706 707 // Event: Change Region on ?region=Europe 708 const REGION_ID = OUMUtils.getParameterByName("region"); 709 if (btn.textContent == REGION_ID) { 710 btn.click(); 711 } 712 }); 713 } 714 715 // Public interface 716 return { 717 init: function (mapEl) { 718 try { 719 initializeStartPosition(); 720 721 // Initialize map 722 map = L.map(mapEl, { 723 gestureHandling: oum_enable_scrollwheel_zoom_map ? false : true, 724 minZoom: 1, // Set default minimum zoom 725 attributionControl: true, 726 fullscreenControl: oum_enable_fullscreen, 727 fullscreenControlOptions: { 728 position: "topleft", 729 fullscreenElement: document.querySelector( 730 ".open-user-map .map-wrap" 731 ), 732 }, 733 }); 734 735 map.attributionControl.setPrefix(false); 736 737 // First set up the tile layer 738 setupTileLayer(OUMConfig.getMapStyle()); 739 740 // Calculate initial bounds based on settings map dimensions 741 const boundsArray = OUMUtils.latLngToBounds( 742 startPosition.lat, 743 startPosition.lng, 744 startPosition.zoom, 745 570, // Width of settings map 746 372 // Height of settings map 747 ); 748 749 // Convert to Leaflet bounds 750 const initialBounds = L.latLngBounds( 751 L.latLng(boundsArray[0][0], boundsArray[0][1]), 752 L.latLng(boundsArray[1][0], boundsArray[1][1]) 753 ); 754 755 // Set view to match settings map exactly, with zoom offset 756 const zoomOffset = 0.7; // Zoom in a bit more to match settings map 757 map.fitBounds(initialBounds, { 758 animate: false, 759 padding: [0, 0], // No padding to match exactly 760 maxZoom: map.getBoundsZoom(initialBounds) + zoomOffset, 761 }); 762 763 // Set up other components 764 setupMapBounds(); 765 setupMapEvents(); 766 setupRegionEvents(); 767 768 window.oumMap = map; 769 770 // Listen for markers initialized event 771 document.addEventListener('oum:markers_initialized', function(e) { 772 if (e.detail.mapId === mapEl) { 773 // Set up controls after markers are ready 774 setupControls(); 775 } 776 }, { once: true }); // Use once: true to ensure it only runs once 777 778 // Dispatch map initialized event when everything is ready 779 document.dispatchEvent(new CustomEvent('oum:map_initialized', { 780 detail: { 781 mapId: mapEl, 782 map: oumMap 783 } 784 })); 785 786 return map; 787 } catch (error) { 788 OUMErrorHandler.showError("Error initializing map: " + error.message); 789 throw error; 790 } 791 }, 792 getMap: function () { 793 return map; 794 }, 795 getStartPosition: function () { 796 return startPosition; 797 }, 798 }; 799})(); 800 801/** 802 * Markers Module - Handles all marker-related functionality 803 */ 804const OUMMarkers = (function () { 805 // Private variables 806 let markersLayer = null; 807 let allMarkers = []; 808 let map = null; 809 810 // Private functions 811 function initializeMarkersLayer() { 812 markersLayer = !oum_enable_cluster 813 ? L.layerGroup({ chunkedLoading: true }) 814 : L.markerClusterGroup({ 815 showCoverageOnHover: false, 816 removeOutsideVisibleBounds: false, 817 maxClusterRadius: 40, 818 chunkedLoading: true, 819 }); 820 } 821 822 function createMarker(location) { 823 const contentText = ( 824 location.title + 825 " | " + 826 location.content.replace(/(<([^>]+)>)/gi, " ").replace(/\s\s+/g, " ") 827 ).toLowerCase(); 828 829 let marker = L.marker([location.lat, location.lng], { 830 title: location.title, 831 post_id: location.post_id, 832 content: contentText, 833 icon: L.icon({ 834 iconUrl: location.icon, 835 iconSize: [26, 41], 836 iconAnchor: [13, 41], 837 popupAnchor: [0, -25], 838 shadowUrl: marker_shadow_url, 839 shadowSize: [41, 41], 840 shadowAnchor: [13, 41], 841 }), 842 types: location.types || [], 843 }); 844 845 let popup = L.responsivePopup().setContent(location.content); 846 marker.bindPopup(popup); 847 848 return marker; 849 } 850 851 function setupMarkerEvents() { 852 // Event: Open Location Bubble 853 map.on("popupopen", function (locationBubble) { 854 const el = document.querySelector(
855 ".open-user-map #location-fullscreen-container" 856 ); 857 el.querySelector(".location-content-wrap").innerHTML = 858 locationBubble.popup.getContent(); 859 el.classList.add("visible"); 860 document.querySelector("body").classList.add("oum-location-opened"); 861 }); 862 863 // Event: Close Location Bubble 864 map.on("popupclose", function () { 865 const el = document.querySelector( 866 ".open-user-map #location-fullscreen-container" 867 ); 868 // Clear the content to stop any media playback (YouTube, Vimeo, etc.) 869 el.querySelector(".location-content-wrap").innerHTML = ''; 870 871 el.classList.remove("visible"); 872 document.querySelector("body").classList.remove("oum-location-opened"); 873 }); 874 } 875 876 function filterMarkers() { 877 // Get filter values 878 const markerFilterInput = document.getElementById("oum_filter_markers"); 879 const filter = markerFilterInput 880 ? markerFilterInput.value.toLowerCase() 881 : ""; 882 const categoryInputs = document.querySelectorAll( 883 '.open-user-map .oum-filter-controls [name="type"]' 884 ); 885 const checkedCategories = Array.from(categoryInputs) 886 .filter((input) => input.checked) 887 .map((input) => input.value); 888 889 // Clear existing markers 890 markersLayer.clearLayers(); 891 892 // Filter and re-add markers 893 allMarkers.forEach((marker) => { 894 const contentText = marker.options.content.toLowerCase(); 895 const markerTypes = marker.options.types || []; 896 897 const matchesTextFilter = !filter || contentText.includes(filter); 898 const matchesCategoryFilter = 899 markerTypes.length === 0 || 900 (checkedCategories.length === 0 && markerTypes.length === 0) || 901 markerTypes.some((type) => checkedCategories.includes(type)); 902 903 if (matchesTextFilter && matchesCategoryFilter) { 904 markersLayer.addLayer(marker); 905 } 906 }); 907 } 908 909 function setupFilterListEvents() { 910 const filterControls = document.querySelector( 911 ".open-user-map .oum-filter-controls" 912 ); 913 if (!filterControls) return; 914 915 // Function to show the filter list 916 function showFilterList() { 917 filterControls.classList.add("active"); 918 } 919 920 // Function to hide the filter list 921 function hideFilterList() { 922 filterControls.classList.remove("active"); 923 } 924 925 // Event: Open Filter List (mouseover for collapsed design) 926 if (filterControls.classList.contains("use-collapse")) { 927 filterControls 928 .querySelector(".oum-filter-toggle") 929 ?.addEventListener("mouseover", showFilterList); 930 filterControls 931 .querySelector(".oum-filter-list") 932 ?.addEventListener("mouseleave", hideFilterList); 933 } 934 935 // Event: Open Filter List (click) 936 filterControls 937 .querySelector(".oum-filter-toggle") 938 ?.addEventListener("click", showFilterList); 939 940 // Event: Close Filter List (click on close button) 941 filterControls 942 .querySelector(".oum-filter-list .close-filter-list") 943 ?.addEventListener("click", hideFilterList); 944 } 945 946 function handleAutoOpenMarker(markerId) { 947 const targetMarker = allMarkers.find((m) => m.options.post_id === markerId); 948 if (targetMarker) { 949 if (oum_enable_cluster) { 950 // For clustered markers: 951 // First zoom to the marker's location
952 map.setView(targetMarker.getLatLng(), oum_searchmarkers_zoom - 1); 953 954 // Then wait a bit for the clustering to update 955 setTimeout(() => { 956 // Try to zoom to the specific marker and show it 957 markersLayer.zoomToShowLayer(targetMarker, () => { 958 targetMarker.openPopup(); 959 }); 960 }, 500); 961 } else { 962 // For non-clustered markers, we can open the popup directly 963 map.setView(targetMarker.getLatLng(), oum_searchmarkers_zoom); 964 targetMarker.openPopup(); 965 } 966 } 967 } 968 969 // Public interface 970 return { 971 init: function (mapInstance) { 972 map = mapInstance; 973 initializeMarkersLayer(); 974 markersLayer.addTo(map); 975 setupMarkerEvents(); 976 setupFilterListEvents(); 977 978 // Make layer globally available (for backward compatibility) 979 window.oumMarkersLayer = markersLayer; 980 window.oumAllMarkers = allMarkers; 981 982 // Dispatch markers initialized event 983 document.dispatchEvent(new CustomEvent('oum:markers_initialized', { 984 detail: { 985 mapId: map._container.id, 986 markersLayer: markersLayer 987 } 988 })); 989 990 return this; 991 }, 992 addMarkers: function (locations) { 993 locations.forEach((location) => { 994 const marker = createMarker(location); 995 allMarkers.push(marker); 996 markersLayer.addLayer(marker); 997 }); 998 999 // After adding all markers, check if we need to auto-open one 1000 const POPUP_MARKER_ID = OUMUtils.getParameterByName("markerid"); 1001 if (POPUP_MARKER_ID) { 1002 handleAutoOpenMarker(POPUP_MARKER_ID); 1003 } 1004 }, 1005 filterMarkers: filterMarkers, 1006 getMarkersLayer: function () { 1007 return markersLayer; 1008 }, 1009 getAllMarkers: function () { 1010 return allMarkers; 1011 }, 1012 }; 1013})(); 1014 1015/** 1016 * Form Map Module - Handles all map-related functionality for the form 1017 */ 1018const OUMFormMap = (function () { 1019 // Private variables 1020 let formMap = null; 1021 let locationMarker = null; 1022 let markerIsVisible = false; 1023 let isAdjusting = false; 1024 let isInitialized = false; 1025 1026 // Private functions 1027 function initializeFormMap() { 1028 if (isInitialized) { 1029 return; 1030 } 1031 1032 formMap = L.map("mapGetLocation", { 1033 attributionControl: false, 1034 gestureHandling: true, 1035 minZoom: 1, 1036 zoomSnap: 1, 1037 zoomDelta: 1, 1038 fullscreenControl: oum_enable_fullscreen, 1039 fullscreenControlOptions: { 1040 position: "topleft", 1041 }, 1042 }); 1043 1044 // Make form map globally available (for backward compatibility) 1045 window.oumMap2 = formMap; 1046 1047 setupTileLayer(); 1048 setupControls(); 1049 setupMarker(); 1050 setupMapEvents(); 1051 1052 // Always apply bounds to prevent showing repeated maps 1053 const boundsToUse = oum_enable_fixed_map_bounds 1054 ? sharedMapBounds 1055 : OUMConfig.defaults.map.bounds; 1056 1057 // Set the bounds 1058 formMap.setMaxBounds(boundsToUse); 1059 1060 // Set minimum zoom level based on bounds 1061 const maxVisibleBounds = formMap.getBoundsZoom(boundsToUse); 1062 formMap.setMinZoom(maxVisibleBounds); 1063 1064 // Add moveend event to enforce bounds 1065 formMap.on("moveend", function () { 1066 if (isAdjusting) return; 1067 isAdjusting = true; 1068 1069 const zoom = formMap.getZoom(); 1070 1071 // Only enforce bounds if we're zoomed in beyond the minimum zoom 1072 if (zoom > maxVisibleBounds) { 1073 const currentBounds = formMap.getBounds(); 1074 const currentCenter = formMap.getCenter(); 1075 1076 let needsAdjustment = false; 1077 let newLat = currentCenter.lat; 1078 let newLng = currentCenter.lng; 1079 1080 // Calculate current viewport dimensions 1081 const viewportHeight = currentBounds.getNorth() - currentBounds.getSouth(); 1082 const viewportWidth = currentBounds.getEast() - currentBounds.getWest(); 1083 1084 // Check and adjust latitude (north/south) 1085 if (currentBounds.getNorth() > boundsToUse.getNorth()) { 1086 newLat = boundsToUse.getNorth() - viewportHeight / 2; 1087 needsAdjustment = true; 1088 } else if (currentBounds.getSouth() < boundsToUse.getSouth()) {
1089 newLat = boundsToUse.getSouth() + viewportHeight / 2; 1090 needsAdjustment = true; 1091 } 1092 1093 // Check and adjust longitude (east/west) 1094 if (currentBounds.getEast() > boundsToUse.getEast()) { 1095 newLng = boundsToUse.getEast() - viewportWidth / 2; 1096 needsAdjustment = true; 1097 } else if (currentBounds.getWest() < boundsToUse.getWest()) { 1098 newLng = boundsToUse.getWest() + viewportWidth / 2; 1099 needsAdjustment = true; 1100 } 1101 1102 if (needsAdjustment) { 1103 formMap.setView([newLat, newLng], zoom, { animate: false }); 1104 } 1105 } 1106 1107 isAdjusting = false; 1108 }); 1109 1110 isInitialized = true; 1111 } 1112 1113 function setupTileLayer() { 1114 // Default to OpenStreetMap if mapStyle is undefined 1115 const mapStyle = window.mapStyle || 'OpenStreetMap.Mapnik'; 1116 1117 if (mapStyle === "Custom1") { 1118 L.tileLayer("https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png").addTo(formMap); 1119 L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png", { 1120 tileSize: 512, 1121 zoomOffset: -1, 1122 }).addTo(formMap); 1123 } else if (mapStyle === "Custom2") { 1124 L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png").addTo(formMap); 1125 L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png", { 1126 tileSize: 512, 1127 zoomOffset: -1, 1128 }).addTo(formMap); 1129 } else if (mapStyle === "Custom3") { 1130 L.tileLayer("https://{s}.basemaps.cartocdn.com/dark_nolabels/{z}/{x}/{y}.png").addTo(formMap); 1131 L.tileLayer("https://{s}.basemaps.cartocdn.com/rastertiles/voyager_only_labels/{z}/{x}/{y}{r}.png", { 1132 tileSize: 512, 1133 zoomOffset: -1, 1134 }).addTo(formMap); 1135 } else if (mapStyle.startsWith("MapBox.")) { 1136 L.tileLayer.provider("MapBox", { 1137 id: mapStyle.replace("MapBox.", "mapbox/") + (mapStyle.includes("-v") ? "" : "-v12"), 1138 accessToken: OUMConfig.getTileProviderKey(), 1139 }).addTo(formMap); 1140 } else { 1141 // Default 1142 L.tileLayer.provider(mapStyle).addTo(formMap); 1143 } 1144 } 1145 1146 function setupControls() { 1147 // Add searchbar: address 1148 const search = new GeoSearch.GeoSearchControl({ 1149 style: "bar", 1150 showMarker: false, 1151 provider: OUMUtils.initGeosearchProvider(), 1152 searchLabel: oum_searchaddress_label, 1153 updateMap: false, 1154 }); 1155 formMap.addControl(search); 1156 1157 // Add control: get current location 1158 if (oum_enable_currentlocation) { 1159 window.map2_locate_process = L.control.locate({ 1160 flyTo: true, 1161 showPopup: false, 1162 }).addTo(formMap); 1163 } 1164 } 1165 1166 function setupMarker() { 1167 // Marker Icon 1168 let markerIcon = L.icon({ 1169 iconUrl: marker_icon_url, 1170 iconSize: [26, 41], 1171 iconAnchor: [13, 41], 1172 popupAnchor: [0, -25], 1173 shadowUrl: marker_shadow_url, 1174 shadowSize: [41, 41], 1175 shadowAnchor: [13, 41], 1176 }); 1177 1178 locationMarker = L.marker([0, 0], { 1179 icon: markerIcon, 1180 draggable: true, 1181 }); 1182 1183 // Make marker globally available (for backward compatibility) 1184 window.locationMarker = locationMarker; 1185 window.markerIsVisible = markerIsVisible; 1186 1187 // Event: drag marker 1188 locationMarker.on("dragend", function (e) { 1189 setLocationLatLng(e.target.getLatLng()); 1190 }); 1191 } 1192 1193 function setupMapEvents() { 1194 // Event: click on map to set marker OR location found 1195 formMap.on("click locationfound", function (e) { 1196 let coords = e.latlng; 1197 locationMarker.setLatLng(coords); 1198 1199 if (!markerIsVisible) { 1200 locationMarker.addTo(formMap); 1201 markerIsVisible = true; 1202 window.markerIsVisible = true; 1203 } 1204 1205 setLocationLatLng(coords); 1206 }); 1207 1208 // Event: geosearch success 1209 formMap.on("geosearch/showlocation", handleGeosearchSuccess); 1210 } 1211 1212 function handleGeosearchSuccess(e) { 1213 let coords = e.marker._latlng; 1214 let label = e.location.label; 1215 let isInBounds = formMap.getBounds().contains(coords); 1216 const searchBar = document.querySelector(`#mapGetLocation .leaflet-geosearch-bar form`); 1217 1218 if (!isInBounds && oum_enable_fixed_map_bounds) { 1219 console.log("This search result is out of reach."); 1220 searchBar.style.boxShadow = "0 0 10px rgb(255, 111, 105)"; 1221 setTimeout(function () { 1222 searchBar.style.boxShadow = "0 1px 5px rgba(255, 255, 255, 0.65)"; 1223 }, 2000); 1224 } else { 1225 handleValidGeosearchResult(e.location); 1226 locationMarker.setLatLng(coords); 1227 1228 if (!markerIsVisible) { 1229 locationMarker.addTo(formMap); 1230 markerIsVisible = true; 1231 window.markerIsVisible = true; 1232 } 1233 1234 setLocationLatLng(coords); 1235 } 1236 } 1237 1238 function handleValidGeosearchResult(location) { 1239 if (location.bounds !== null) { 1240 formMap.flyToBounds(location.bounds); 1241 } else if (location.raw.mapView) { 1242 formMap.flyToBounds([ 1243 [location.raw.mapView.south, location.raw.mapView.west], 1244 [location.raw.mapView.north, location.raw.mapView.east], 1245 ]); 1246 } else { 1247 formMap.flyTo([location.y, location.x], 17); 1248 } 1249 } 1250 1251 function setLocationLatLng(markerLatLng) { 1252 document.getElementById("oum_location_lat").value = markerLatLng.lat; 1253 document.getElementById("oum_location_lng").value = markerLatLng.lng; 1254 } 1255 1256 // Public interface 1257 return { 1258 init: function() { 1259 if (!document.getElementById("mapGetLocation")) { 1260 return; 1261 } 1262 initializeFormMap(); 1263 },
1264 setLocation: function(lat, lng) { 1265 if (!locationMarker) return; 1266 locationMarker.setLatLng([lat, lng]); 1267 if (!markerIsVisible) { 1268 locationMarker.addTo(formMap); 1269 markerIsVisible = true; 1270 window.markerIsVisible = true; 1271 } 1272 setLocationLatLng({lat, lng}); 1273 }, 1274 clearMarker: function() { 1275 if (locationMarker && formMap) { 1276 locationMarker.remove(); 1277 markerIsVisible = false; 1278 window.markerIsVisible = false; 1279 } 1280 }, 1281 invalidateSize: function() { 1282 if (formMap) { 1283 formMap.invalidateSize(); 1284 } 1285 }, 1286 getMap: function() { 1287 return formMap; 1288 }, 1289 setView: function(lat, lng, zoom) { 1290 if (formMap) { 1291 formMap.setView([lat, lng], zoom); 1292 } 1293 } 1294 }; 1295})(); 1296 1297/** 1298 * Form Controller Module - Handles all form-related functionality 1299 */ 1300const OUMFormController = (function () { 1301 // Private variables 1302 let isEditMode = false; 1303 let currentLocationId = null; 1304 let selectedFiles = []; 1305 1306 // Private functions 1307 function showFormMessage(type, headline, message, buttonText = null, buttonCallback = null) { 1308 const form = document.getElementById('oum_add_location'); 1309 const errorDiv = document.getElementById('oum_add_location_error'); 1310 const thankyouDiv = document.getElementById('oum_add_location_thankyou'); 1311 1312 if (!form || !errorDiv || !thankyouDiv) { 1313 console.error('Required form elements not found'); 1314 return; 1315 } 1316 1317 // Hide form and error 1318 form.style.display = 'none'; 1319 errorDiv.style.display = 'none'; 1320 1321 // Update thank you message 1322 const headlineEl = thankyouDiv.querySelector('h3'); 1323 const messageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text'); 1324 const buttonEl = thankyouDiv.querySelector('button'); 1325 1326 if (!headlineEl || !messageEl || !buttonEl) { 1327 // Create elements if they don't exist 1328 if (!headlineEl) { 1329 const newHeadline = document.createElement('h3'); 1330 thankyouDiv.appendChild(newHeadline); 1331 } 1332 if (!messageEl) { 1333 const newMessage = document.createElement('p'); 1334 newMessage.className = 'oum-add-location-thankyou-text'; 1335 thankyouDiv.appendChild(newMessage); 1336 } 1337 if (!buttonEl) { 1338 const newButton = document.createElement('button'); 1339 thankyouDiv.appendChild(newButton); 1340 } 1341 } 1342 1343 // Get elements again (they should exist now) 1344 const finalHeadlineEl = thankyouDiv.querySelector('h3'); 1345 const finalMessageEl = thankyouDiv.querySelector('.oum-add-location-thankyou-text'); 1346 const finalButtonEl = thankyouDiv.querySelector('button'); 1347 1348 // Add specific class for delete confirmation 1349 thankyouDiv.className = type === 'confirm-delete' ? 'oum-delete-confirmation' : ''; 1350 1351 if (finalHeadlineEl) finalHeadlineEl.textContent = headline || ''; 1352 if (finalMessageEl) finalMessageEl.textContent = message || ''; 1353 1354 // Handle button 1355 if (finalButtonEl) { 1356 if (buttonText && buttonCallback) { 1357 finalButtonEl.textContent = buttonText; 1358 finalButtonEl.onclick = buttonCallback; 1359 finalButtonEl.style.display = 'inline-block'; 1360 } else { 1361 finalButtonEl.style.display = 'none'; 1362 } 1363 } 1364 1365 thankyouDiv.style.display = 'block'; 1366 } 1367 1368 function setupDeleteButton() { 1369 const deleteBtn = document.getElementById('oum_delete_location_btn'); 1370 if (deleteBtn) { 1371 deleteBtn.addEventListener('click', function(e) { 1372 e.preventDefault(); 1373 1374 // Show confirmation using the message system 1375 showFormMessage( 1376 'confirm-delete', 1377 wp.i18n.__('Delete this location?', 'open-user-map'), 1378 wp.i18n.__('This action cannot be undone. The location will be permanently removed from the map.', 'open-user-map'), 1379 wp.i18n.__('Yes, delete location', 'open-user-map'), 1380 function() { 1381 // Set delete flag 1382 document.getElementById('oum_delete_location').value = 'true'; 1383 1384 // Get the form 1385 const form = document.getElementById('oum_add_location'); 1386 const formData = new FormData(form); 1387 formData.append('action', 'oum_add_location_from_frontend'); 1388 1389 // Submit via AJAX 1390 jQuery.ajax({ 1391 type: 'POST', 1392 url: oum_ajax.ajaxurl, 1393 cache: false, 1394 contentType: false, 1395 processData: false, 1396 data: formData, 1397 success: function(response) { 1398 if (response.success) { 1399 showFormMessage( 1400 'success',
1401 wp.i18n.__('Location deleted', 'open-user-map'), 1402 wp.i18n.__('The location has been successfully removed from the map.', 'open-user-map'), 1403 wp.i18n.__('Close and refresh map', 'open-user-map'), 1404 function() { 1405 window.location.reload(); 1406 } 1407 ); 1408 } else { 1409 oumShowError(response.data); 1410 } 1411 }, 1412 error: function(XMLHttpRequest, textStatus, errorThrown) { 1413 console.log(errorThrown); 1414 oumShowError([{message: wp.i18n.__('An error occurred while deleting the location. Please try again.', 'open-user-map')}]); 1415 } 1416 }); 1417 } 1418 ); 1419 }); 1420 } 1421 } 1422 1423 function setupFormEvents() { 1424 // Event: click on "+ Add Location" button 1425 const addLocationBtn = document.getElementById("open-add-location-overlay"); 1426 if (addLocationBtn) { 1427 addLocationBtn.addEventListener("click", handleAddLocationClick); 1428 } 1429 1430 // Event: click on "Edit Location" button 1431 document.addEventListener('click', function(e) { 1432 if (e.target && e.target.classList.contains('edit-location-button')) { 1433 e.preventDefault(); 1434 const locationId = e.target.getAttribute('data-post-id'); 1435 const location = window.oum_all_locations.find(loc => loc.post_id === locationId); 1436 1437 if (location) { 1438 resetForm(); 1439 openForm(location); 1440 } 1441 } 1442 }); 1443 1444 setupCloseEvents(); 1445 setupNotificationEvents(); 1446 setupMediaEvents(); 1447 setupValidation(); 1448 } 1449 1450 function handleAddLocationClick() { 1451 resetForm(); 1452 openForm(); 1453 } 1454 1455 function openForm(location = null) { 1456 document.querySelector(".add-location").classList.add("active"); 1457 document.body.classList.add("oum-add-location-opened"); 1458 1459 setTimeout(function () { 1460 // Initialize map if needed 1461 OUMFormMap.init(); 1462 1463 if (location) { 1464 populateForm(location); 1465 } else { 1466 // Set view to match main map 1467 const mainMapEl = document.querySelector(`#${map_el}`); 1468 if (mainMapEl) { 1469 const mainMap = window.oumMap; 1470 const mainCenter = mainMap.getCenter(); 1471 const mainZoom = mainMap.getZoom(); 1472 OUMFormMap.setView(mainCenter.lat, mainCenter.lng, mainZoom); 1473 } 1474 } 1475 1476 // Add a separate timeout for invalidateSize 1477 setTimeout(() => { 1478 OUMFormMap.invalidateSize(); 1479 }, 200); 1480 }, 150); 1481 } 1482 1483 function setupCloseEvents() { 1484 const closeBtn = document.getElementById("close-add-location-overlay"); 1485 if (!closeBtn) return; 1486 1487 // Close button click 1488 closeBtn.addEventListener("click", closeForm); 1489 1490 // ESC key 1491 document.addEventListener("keydown", function(evt) { 1492 evt = evt || window.event; 1493 if (evt.key === "Escape" && document.getElementById("add-location-overlay").classList.contains("active")) { 1494 closeForm(); 1495 } 1496 }); 1497 1498 // Backdrop click 1499 document.getElementById("add-location-overlay").addEventListener("click", function(event) { 1500 if (event.target === this) { 1501 closeForm(); 1502 } 1503 }); 1504 } 1505 1506 function closeForm() { 1507 const addLocationOverlay = document.getElementById("add-location-overlay"); 1508 if (addLocationOverlay) { 1509 addLocationOverlay.classList.remove("active"); 1510 } 1511 1512 // Stop locate process 1513 if (window.map2_locate_process) { 1514 window.map2_locate_process.stop(); 1515 } 1516 1517 // Allow body scrolling 1518 document.querySelector("body").classList.remove("oum-add-location-opened"); 1519 1520 // Reset form and clear marker 1521 resetForm(); 1522 OUMFormMap.clearMarker(); 1523 } 1524 1525 function setupNotificationEvents() { 1526 const notificationCheckbox = document.getElementById("oum_location_notification"); 1527 if (notificationCheckbox) { 1528 notificationCheckbox.addEventListener("change", function() { 1529 const authorFields = document.getElementById("oum_author"); 1530 const nameField = document.getElementById("oum_location_author_name"); 1531 const emailField = document.getElementById("oum_location_author_email"); 1532 1533 if (this.checked) { 1534 authorFields.classList.add("active"); 1535 nameField.required = true; 1536 emailField.required = true; 1537 } else { 1538 authorFields.classList.remove("active"); 1539 nameField.required = false;
1540 emailField.required = false; 1541 } 1542 }); 1543 } 1544 } 1545 1546 function setupMediaEvents() { 1547 // Image upload 1548 const imageInput = document.getElementById("oum_location_images"); 1549 if (imageInput) { 1550 // Let OUMMedia handle the image upload 1551 OUMMedia.initializeImageUpload(imageInput); 1552 } 1553 1554 // Remove image button 1555 const removeImageBtn = document.getElementById("oum_remove_image"); 1556 if (removeImageBtn) { 1557 removeImageBtn.addEventListener("click", function() { 1558 document.getElementById("oum_location_images_preview").innerHTML = ""; 1559 }); 1560 } 1561 1562 // Remove audio button 1563 const removeAudioBtn = document.getElementById("oum_remove_audio"); 1564 if (removeAudioBtn) { 1565 removeAudioBtn.addEventListener("click", function() { 1566 const audioInput = document.getElementById("oum_location_audio"); 1567 const previewContainer = audioInput.nextElementSibling; 1568 const previewDiv = previewContainer.querySelector('.audio-preview'); 1569 1570 // Clear the file input 1571 audioInput.value = ""; 1572 1573 // Clear the preview 1574 if (previewDiv) { 1575 previewDiv.innerHTML = ''; 1576 } 1577 1578 // Remove active state 1579 previewContainer.classList.remove("active"); 1580 1581 // Set remove flag 1582 document.getElementById("oum_remove_existing_audio").value = "1"; 1583 }); 1584 } 1585 } 1586 1587 function setupValidation() { 1588 // Add validation logic here 1589 } 1590 1591 function populateForm(location) { 1592 isEditMode = true; 1593 currentLocationId = location.post_id; 1594 1595 // Add edit-location class 1596 const addLocationEl = document.querySelector(".add-location"); 1597 if (addLocationEl) { 1598 addLocationEl.classList.add("edit-location"); 1599 } 1600 1601 // Set post_id 1602 const postIdField = document.getElementById("oum_post_id"); 1603 if (postIdField) { 1604 postIdField.value = location.post_id; 1605 } 1606 1607 // Basic fields 1608 const titleField = document.getElementById("oum_location_title"); 1609 const latField = document.getElementById("oum_location_lat"); 1610 const lngField = document.getElementById("oum_location_lng"); 1611 const addressField = document.getElementById("oum_location_address"); 1612 1613 if (titleField) titleField.value = location.title || ""; 1614 if (latField) latField.value = location.lat || ""; 1615 if (lngField) lngField.value = location.lng || ""; 1616 if (addressField) addressField.value = location.address || ""; 1617 1618 // Marker types/categories 1619 if (location.types && Array.isArray(location.types)) { 1620 if (typeof oum_enable_multiple_marker_types !== 'undefined' && oum_enable_multiple_marker_types == 'true') { 1621 // Handle multiple marker types (checkboxes) 1622 const checkboxes = document.querySelectorAll('input[name="oum_marker_icon[]"]'); 1623 if (checkboxes.length > 0) { 1624 checkboxes.forEach(checkbox => { 1625 checkbox.checked = location.types.includes(checkbox.value); 1626 }); 1627 } 1628 } else { 1629 // Handle single marker type (select) 1630 const markerSelect = document.querySelector('select#oum_marker_icon'); 1631 if (markerSelect) { 1632 markerSelect.value = location.types[0] || ''; 1633 } 1634 } 1635 } 1636 1637 // Description 1638 const descriptionField = document.getElementById("oum_location_text"); 1639 if (descriptionField) { 1640 descriptionField.value = location.text || ""; 1641 } 1642 1643 // Video field 1644 const videoField = document.getElementById("oum_location_video"); 1645 if (videoField && location.video) { 1646 videoField.value = location.video; 1647 } 1648 1649 // Handle custom fields 1650 if (location.custom_fields && Array.isArray(location.custom_fields)) { 1651 location.custom_fields.forEach(field => { 1652 if (!field || typeof field.index === 'undefined') return; 1653 1654 if (field.fieldtype === 'checkbox') { 1655 const fieldValues = Array.isArray(field.val) ? field.val : [field.val]; 1656 const checkboxes = document.querySelectorAll( 1657 `input[type="checkbox"][name="oum_location_custom_fields[${field.index}][]"]` 1658 ); 1659 1660 if (checkboxes.length > 0) {
1661 checkboxes.forEach(checkbox => { 1662 if (checkbox) { 1663 const checkboxValue = checkbox.value.trim(); 1664 const normalizedValues = fieldValues.map(val => (val || '').toString().trim()); 1665 checkbox.checked = normalizedValues.includes(checkboxValue); 1666 } 1667 }); 1668 } 1669 } else if (field.fieldtype === 'radio') { 1670 const radioInputs = document.querySelectorAll( 1671 `input[type="radio"][name="oum_location_custom_fields[${field.index}]"]` 1672 ); 1673 1674 if (radioInputs.length > 0) { 1675 radioInputs.forEach(radio => { 1676 if (radio) { 1677 radio.checked = radio.value === field.val; 1678 } 1679 }); 1680 } 1681 } else { 1682 const input = document.querySelector(`[name="oum_location_custom_fields[${field.index}]"]`); 1683 if (input) { 1684 input.value = field.val || ''; 1685 } 1686 } 1687 }); 1688 } 1689 1690 // Handle images 1691 if (location.image) { 1692 const imageUrls = location.image.split('|').filter(url => url); 1693 const previewContainer = document.getElementById('oum_location_images_preview'); 1694 1695 if (previewContainer) { 1696 previewContainer.innerHTML = ''; 1697 1698 imageUrls.forEach(url => { 1699 if (!url) return; 1700 1701 const previewItem = document.createElement('div'); 1702 previewItem.className = 'image-preview-item existing-image'; 1703 previewItem.dataset.url = url; 1704 1705 previewItem.innerHTML = ` 1706 <img src="${url}" alt="Preview"> 1707 <div class="remove-image" title="Remove image">×</div> 1708 <div class="drag-handle" title="Drag to reorder">â®â®</div> 1709 <input type="hidden" name="existing_images[]" value="${url}"> 1710 `; 1711 1712 // Add event listener for remove button 1713 const removeButton = previewItem.querySelector('.remove-image'); 1714 if (removeButton) { 1715 removeButton.addEventListener('click', OUMMedia.handleRemoveImage); 1716 } 1717 1718 // Set up drag and drop for existing images 1719 OUMMedia.setupDragAndDrop(previewItem); 1720 1721 previewItem.style.opacity = "0"; 1722 previewItem.style.transform = "scale(0.9)"; 1723 previewItem.style.transition = "all 0.3s ease"; 1724 previewItem.style.opacity = "1"; 1725 previewItem.style.transform = "scale(1)"; 1726 previewContainer.appendChild(previewItem); 1727 }); 1728 } 1729 } 1730 1731 // Handle audio 1732 if (location.audio) { 1733 OUMMedia.setExistingAudio(location.audio); 1734 document.getElementById("oum_remove_existing_audio").value = "0"; 1735 } 1736 1737 // Set map view to location 1738 if (location.lat && location.lng) { 1739 OUMFormMap.setView(location.lat, location.lng, 16); 1740 OUMFormMap.setLocation(location.lat, location.lng); 1741 } 1742 } 1743 1744 function resetForm() { 1745 isEditMode = false; 1746 currentLocationId = null; 1747 selectedFiles = []; 1748 window.oumSelectedFiles = []; 1749 1750 const addLocationEl = document.querySelector(".add-location"); 1751 if (addLocationEl) { 1752 addLocationEl.classList.remove("edit-location"); 1753 } 1754 1755 // Reset form and message system 1756 const form = document.getElementById("oum_add_location"); 1757 const errorDiv = document.getElementById("oum_add_location_error"); 1758 const thankyouDiv = document.getElementById("oum_add_location_thankyou"); 1759 1760 // Reset form if it exists 1761 if (form) { 1762 form.reset(); 1763 form.style.display = 'block'; 1764 } 1765 1766 // Reset custom fields 1767 const customFields = document.querySelectorAll('[name^="oum_location_custom_fields"]'); 1768 if (customFields) { 1769 customFields.forEach(field => { 1770 if (field.type === 'checkbox' || field.type === 'radio') { 1771 field.checked = false;
1772 } else if (field.type === 'select-one') { 1773 field.selectedIndex = 0; 1774 } else { 1775 field.value = ''; 1776 } 1777 }); 1778 } 1779 1780 // Reset post_id field 1781 const postIdField = document.getElementById("oum_post_id"); 1782 if (postIdField) { 1783 postIdField.value = ""; 1784 } 1785 1786 // Reset oum_delete_location field 1787 const deleteLocationField = document.getElementById("oum_delete_location"); 1788 if (deleteLocationField) { 1789 deleteLocationField.value = ""; 1790 } 1791 1792 // Reset error message if it exists 1793 if (errorDiv) { 1794 errorDiv.style.display = 'none'; 1795 } 1796 1797 // Reset thank you message if it exists 1798 if (thankyouDiv) { 1799 thankyouDiv.style.display = 'none'; 1800 thankyouDiv.classList.remove('oum-delete-confirmation'); 1801 } 1802 1803 // Reset image preview 1804 const previewContainer = document.getElementById("oum_location_images_preview"); 1805 if (previewContainer) { 1806 previewContainer.innerHTML = ""; 1807 } 1808 1809 // Reset audio preview 1810 const audioInput = document.getElementById("oum_location_audio"); 1811 if (audioInput) { 1812 const previewContainer = audioInput.nextElementSibling; 1813 const previewDiv = previewContainer.querySelector('.audio-preview'); 1814 1815 // Clear the file input 1816 audioInput.value = ""; 1817 1818 // Clear the preview 1819 if (previewDiv) { 1820 previewDiv.innerHTML = ''; 1821 } 1822 1823 // Remove active state 1824 previewContainer.classList.remove("active"); 1825 } 1826 1827 // Reset hidden fields 1828 const removeExistingImage = document.getElementById("oum_remove_existing_image"); 1829 if (removeExistingImage) { 1830 removeExistingImage.value = "0"; 1831 } 1832 1833 const removeExistingAudio = document.getElementById("oum_remove_existing_audio"); 1834 if (removeExistingAudio) { 1835 removeExistingAudio.value = "0"; 1836 } 1837 1838 // Reset author section 1839 const authorSection = document.getElementById("oum_author"); 1840 if (authorSection) { 1841 authorSection.classList.remove("active"); 1842 } 1843 1844 // Reset name and email fields 1845 const nameField = document.getElementById("oum_location_author_name"); 1846 const emailField = document.getElementById("oum_location_author_email"); 1847 if (nameField && emailField) { 1848 nameField.required = false; 1849 emailField.required = false; 1850 } 1851 } 1852 1853 // Public interface 1854 return { 1855 init: function() { 1856 setupFormEvents(); 1857 setupDeleteButton(); // Add delete button handler 1858 }, 1859 showFormMessage: showFormMessage, 1860 openForm: openForm, 1861 closeForm: closeForm, 1862 resetForm: resetForm, 1863 populateForm: populateForm, 1864 isEditMode: function() { 1865 return isEditMode; 1866 }, 1867 getCurrentLocationId: function() { 1868 return currentLocationId; 1869 } 1870 }; 1871})(); 1872 1873/** 1874 * Media Module - Handles image upload and preview functionality 1875 */ 1876const OUMMedia = (function () { 1877 // Private variables 1878 let selectedFiles = []; 1879 let startX, startY, originalPosition, placeholder; 1880 let isDragging = false; 1881 1882 // Private functions 1883 function initializeImageUpload(imageInput) { 1884 if (!imageInput) return; 1885 1886 // Add click handler for upload icon label 1887 const uploadLabel = imageInput.parentElement.querySelector( 1888 'label[for="oum_location_images"]' 1889 ); 1890 if (uploadLabel) { 1891 uploadLabel.addEventListener("click", function (e) { 1892 e.preventDefault(); 1893 imageInput.click(); 1894 }); 1895 } 1896 1897 // Setup drag and drop handlers 1898 document.addEventListener("mousemove", handleDragMove); 1899 document.addEventListener("mouseup", handleDragEnd); 1900 if (imageInput.parentElement.querySelector(".image-preview-container")) { 1901 imageInput.parentElement.querySelector(".image-preview-container").addEventListener("dragover", handleDragOver); 1902 } 1903 1904 // Setup image input change handler 1905 imageInput.addEventListener("change", handleImageInputChange); 1906 } 1907 1908 function initializeAudioUpload() { 1909 const audioInput = document.getElementById('oum_location_audio'); 1910 if (!audioInput) return; 1911 1912 // Add click handler for upload icon label 1913 const uploadLabel = audioInput.parentElement.querySelector( 1914 'label[for="oum_location_audio"]' 1915 ); 1916 if (uploadLabel) { 1917 uploadLabel.addEventListener("click", function (e) { 1918 e.preventDefault(); 1919 audioInput.click(); 1920 }); 1921 } 1922 1923 // Setup audio input change handler 1924 audioInput.addEventListener("change", handleAudioInputChange); 1925 } 1926 1927 function handleAudioInputChange(e) { 1928 const audioInput = e.target; 1929 const previewContainer = audioInput.nextElementSibling; 1930 const previewDiv = previewContainer.querySelector('.audio-preview'); 1931 1932 if (audioInput.files && audioInput.files[0]) { 1933 const file = audioInput.files[0]; 1934 1935 previewContainer.classList.add('active'); 1936 1937 // Create audio preview element 1938 const audio = document.createElement('audio'); 1939 audio.controls = true; 1940 audio.style.width = '100%'; 1941 1942 const source = document.createElement('source'); 1943 source.src = URL.createObjectURL(file); 1944 source.type = file.type; 1945 1946 audio.appendChild(source); 1947 1948 // Replace existing audio preview if any 1949 previewDiv.innerHTML = ''; 1950 previewDiv.appendChild(audio); 1951 } 1952 } 1953 1954 function setExistingAudio(audioUrl) { 1955 if (!audioUrl) return; 1956 1957 const audioInput = document.getElementById('oum_location_audio'); 1958 if (!audioInput) return; 1959 1960 const previewContainer = audioInput.nextElementSibling; 1961 const previewDiv = previewContainer.querySelector('.audio-preview'); 1962 1963 previewContainer.classList.add('active'); 1964 1965 // Create audio preview element 1966 const audio = document.createElement('audio'); 1967 audio.controls = true; 1968 audio.style.width = '100%'; 1969 1970 const source = document.createElement('source'); 1971 source.src = audioUrl; 1972 source.type = 'audio/' + audioUrl.split('.').pop(); 1973 1974 audio.appendChild(source); 1975 1976 // Replace existing audio preview if any 1977 previewDiv.innerHTML = ''; 1978 previewDiv.appendChild(audio); 1979 } 1980 1981 function handleImageInputChange(e) { 1982 const imageInput = e.target; 1983 const previewContainer = document.getElementById( 1984 "oum_location_images_preview" 1985 ); 1986 const maxFiles = parseInt(imageInput.dataset.maxFiles) || 5; 1987 const maxFileSize = OUMConfig.defaults.media.maxImageSize; // in bytes 1988 1989 // Convert FileList to Array and store in a variable 1990 const files = Array.prototype.slice.call(e.target.files); 1991 const existingCount = selectedFiles.length; 1992 const totalFiles = existingCount + files.length; 1993 1994 if (totalFiles > maxFiles) { 1995 alert( 1996 wp.i18n.sprintf( 1997 /* translators: %1$d: maximum number of files, %2$d: number of files that will be used */ 1998 wp.i18n.__('Maximum %1$d images allowed. Only the first %2$d new images will be used.', 'open-u
1998ser-map'), 1999 maxFiles, 2000 maxFiles - existingCount 2001 ) 2002 ); 2003 } 2004 2005 // Process only up to remaining slots 2006 const remainingSlots = maxFiles - existingCount; 2007 const filesToProcess = files.slice(0, remainingSlots); 2008 2009 // Validate file sizes and collect valid files 2010 const validFiles = []; 2011 const invalidFiles = []; 2012 2013 filesToProcess.forEach(file => { 2014 if (file.size > maxFileSize) { 2015 invalidFiles.push(file.name); 2016 } else { 2017 validFiles.push(file); 2018 } 2019 }); 2020 2021 // Show error message for invalid files 2022 if (invalidFiles.length > 0) { 2023 const maxSizeMB = Math.round(maxFileSize / (1024 * 1024)); 2024 alert( 2025 wp.i18n.sprintf( 2026 /* translators: %1$d: maximum file size in MB, %2$s: list of files */ 2027 wp.i18n.__('The following images exceed the maximum file size of %1$dMB:\n%2$s', 'open-user-map'), 2028 maxSizeMB, 2029 invalidFiles.join('\n') 2030 ) 2031 ); 2032 } 2033 2034 // Update selected files with only valid ones 2035 selectedFiles = [...selectedFiles, ...validFiles]; 2036 2037 // Create previews for valid files only 2038 createImagePreviews(validFiles, previewContainer); 2039 2040 // Make selectedFiles available globally for the form submission 2041 window.oumSelectedFiles = selectedFiles; 2042 } 2043 2044 function createImagePreviews(files, container) { 2045 files.forEach((file) => { 2046 const reader = new FileReader(); 2047 2048 reader.onload = function (e) { 2049 const previewItem = createPreviewItem(e.target.result, file.name); 2050 2051 // Add the item with a fade-in animation 2052 previewItem.style.opacity = "0"; 2053 previewItem.style.transform = "scale(0.9)"; 2054 container.appendChild(previewItem); 2055 2056 // Trigger animation after a brief delay 2057 setTimeout(() => { 2058 previewItem.style.transition = "all 0.3s ease"; 2059 previewItem.style.opacity = "1"; 2060 previewItem.style.transform = "scale(1)"; 2061 }, 50); 2062 }; 2063 2064 reader.readAsDataURL(file); 2065 }); 2066 } 2067 2068 function createPreviewItem(imgSrc, fileName) { 2069 const previewItem = document.createElement("div"); 2070 previewItem.className = "image-preview-item"; 2071 previewItem.dataset.fileName = fileName; 2072 2073 previewItem.innerHTML = ` 2074 <img src="${imgSrc}" alt="Preview"> 2075 <div class="remove-image" title="Remove image">×</div> 2076 <div class="drag-handle" title="Drag to reorder">â®â®</div> 2077 `; 2078 2079 // Add event listener for remove button 2080 const removeButton = previewItem.querySelector('.remove-image'); 2081 if (removeButton) { 2082 removeButton.addEventListener('click', handleRemoveImage); 2083 } 2084 2085 // Set up drag and drop for existing images 2086 setupDragAndDrop(previewItem); 2087 2088 return previewItem; 2089 } 2090 2091 function handleRemoveImage(e) { 2092 e.preventDefault(); 2093 const previewItem = this.closest(".image-preview-item"); 2094 2095 // If it's an existing image, handle removal differently 2096 if (previewItem.classList.contains("existing-image")) { 2097 const imgUrl = previewItem.querySelector("[name='existing_images[]']").value; 2098 const removedImagesInput = document.getElementById("oum_remove_existing_image"); 2099 const currentValue = removedImagesInput.value === "0" ? [] : removedImagesInput.value.split('|'); 2100 currentValue.push(imgUrl); 2101 removedImagesInput.value = currentValue.join('|'); 2102 } else { 2103 // Remove from selectedFiles array if it's a new image 2104 const fileName = previewItem.dataset.fileName; 2105 selectedFiles = selectedFiles.filter(file => file.name !== fileName); 2106 window.oumSelectedFiles = selectedFiles; 2107 } 2108 2109 // Animate and remove the preview item 2110 previewItem.style.transition = "all 0.3s ease"; 2111 previewItem.style.transform = "scale(0.8)"; 2112 previewItem.style.opacity = "0"; 2113 2114 setTimeout(() => { 2115 previewItem.remove(); 2116 }, 300); 2117 } 2118 2119 function setupDragAndDrop(previewItem) { 2120 previewItem.setAttribute('draggable', 'true'); 2121 2122 previewItem.addEventListener('mousedown', function(e) { 2123 if (e.target.classList.contains('remove-image')) return; 2124 2125 isDragging = true; 2126 this.classList.add('dragging'); 2127 2128 // Get element dimensions once at start 2129 const rect = this.getBoundingClientRect(); 2130 this.style.width = rect.width + 'px'; 2131 this.style.height = rect.height + 'px'; 2132 2133 // Store initial grid container for safety check 2134 this.initialContainer = this.closest('.oum-image-preview-grid'); 2135 2136 // Create placeholder immediately 2137 createPlaceholder(this); 2138 2139 // Set up dragged element 2140 this.style.position = 'fixed'; 2141 this.style.zIndex = '1000'; 2142 this.style.opacity = '0.9'; 2143 this.style.transform = 'scale(1.05) rotate(1deg)'; 2144 this.style.pointerEvents = 'none'; 2145 this.style.boxShadow = '0 5px 15px rgba(0,0,0,0.15)'; 2146 2147 // Set initial position 2148 moveDraggedElement(this, e); 2149 2150 document.body.style.cursor = 'grabbing'; 2151 }); 2152 2153 previewItem.addEventListener('touchstart', handleTouchStart); 2154 } 2155 2156 function createPlaceholder(element) { 2157 placeholder = document.createElement("div"); 2158 placeholder.classList.add("image-preview-placeholder"); 2159 placeholder.style.width = element.offsetWidth + "px"; 2160 placeholder.style.height = element.offsetHeight + "px"; 2161 placeholder.style.transition = "transform 0.2s ease"; 2162 placeholder.style.border = "2px dashed #e02aaf"; 2163 placeholder.style.borderRadius = "4px"; 2164 placeholder.style.backgroundColor = "rgba(224, 42, 175, 0.05)"; 2165 element.parentNode.insertBefore(placeholder, element); 2166 } 2167 2168 function moveDraggedElement(draggable, e) { 2169 const rect = draggable.getBoundingClientRect(); 2170 const centerOffsetX = rect.width / 2; 2171 const centerOffsetY = rect.height / 2; 2172 2173 // Position element directly at cursor with center offset 2174 draggable.style.left = (e.clientX - centerOffsetX) + 'px'; 2175 draggable.style.top = (e.clientY - centerOffsetY) + 'px'; 2176 } 2177 2178 function updatePlaceholderPosition(e) { 2179 const previewContainer = document.getElementById("oum_location_images_preview"); 2180 if (!previewContainer) return; 2181 2182 const draggable = document.querySelector('.dragging'); 2183 if (!draggable) return; 2184 2185 const siblings = [...previewContainer.querySelectorAll(".image-preview-item:not(.dragging)")]; 2186 2187 // Find the closest sibling based on mouse position 2188 const closestSibling = siblings.reduce((closest, child) => { 2189 const rect = child.getBoundingClientRect(); 2190 const centerX = rect.left + rect.width / 2; 2191 const offset = e.clientX - centerX; 2192
2193 if (offset < 0 && (!closest.element || offset > closest.offset)) { 2194 return { offset: offset, element: child }; 2195 } 2196 return closest; 2197 }, { offset: Number.NEGATIVE_INFINITY, element: null }); 2198 2199 if (closestSibling.element) { 2200 previewContainer.insertBefore(placeholder, closestSibling.element); 2201 } else { 2202 previewContainer.appendChild(placeholder); 2203 } 2204 } 2205 2206 function handleDragMove(e) { 2207 if (!isDragging) return; 2208 2209 const draggable = document.querySelector(".dragging"); 2210 if (!draggable) return; 2211 2212 // Update dragged element position 2213 moveDraggedElement(draggable, e); 2214 2215 // Check if cursor is still within any grid container 2216 const gridContainer = document.getElementById("oum_location_images_preview"); 2217 if (!gridContainer) return; 2218 2219 const gridRect = gridContainer.getBoundingClientRect(); 2220 const isWithinGrid = e.clientX >= gridRect.left - 50 && 2221 e.clientX <= gridRect.right + 50 && 2222 e.clientY >= gridRect.top - 50 && 2223 e.clientY <= gridRect.bottom + 50; 2224 2225 // If cursor is outside grid boundaries, hide placeholder 2226 if (!isWithinGrid && placeholder) { 2227 placeholder.style.display = 'none'; 2228 } else if (placeholder) { 2229 placeholder.style.display = 'block'; 2230 updatePlaceholderPosition(e); 2231 } 2232 } 2233 2234 function handleDragEnd() { 2235 const draggable = document.querySelector(".dragging"); 2236 if (!draggable) return; 2237 2238 // Reset cursor 2239 document.body.style.cursor = ""; 2240 2241 // Check if we're still within the grid 2242 const gridContainer = document.getElementById("oum_location_images_preview"); 2243 if (!gridContainer) { 2244 // If no grid found, return item to its initial position 2245 if (draggable.initialContainer) { 2246 draggable.initialContainer.appendChild(draggable); 2247 } 2248 } else { 2249 // Place draggable element at placeholder position if within grid 2250 if (placeholder && placeholder.style.display !== 'none') { 2251 draggable.style.transition = "none"; 2252 placeholder.parentNode.insertBefore(draggable, placeholder); 2253 } else { 2254 // If placeholder is hidden (outside grid), append to end 2255 gridContainer.appendChild(draggable); 2256 } 2257 } 2258 2259 // Remove placeholder 2260 if (placeholder) { 2261 placeholder.remove(); 2262 } 2263 2264 // Reset draggable element styles 2265 draggable.style.position = ""; 2266 draggable.style.zIndex = ""; 2267 draggable.style.top = ""; 2268 draggable.style.left = ""; 2269 draggable.style.width = ""; 2270 draggable.style.height = ""; 2271 draggable.style.transform = ""; 2272 draggable.style.pointerEvents = ""; 2273 draggable.style.boxShadow = ""; 2274 draggable.classList.remove("dragging"); 2275 2276 isDragging = false; 2277 } 2278 2279 function handleTouchStart(e) { 2280 const touch = e.touches[0]; 2281 const mouseEvent = new MouseEvent("mousedown", { 2282 clientX: touch.clientX, 2283 clientY: touch.clientY 2284 }); 2285 this.dispatchEvent(mouseEvent); 2286 } 2287 2288 function handleDragOver(e) { 2289 e.preventDefault(); 2290 e.stopPropagation(); 2291 2292 const draggable = document.querySelector(".dragging"); 2293 if (!draggable) return; 2294 2295 const afterElement = getDragAfterElement(this, e.clientX); 2296 if (afterElement == null) { 2297 this.appendChild(placeholder); 2298 } else { 2299 this.insertBefore(placeholder, afterElement); 2300 } 2301 } 2302 2303 // Public interface 2304 return { 2305 init: function () { 2306 initializeAudioUpload(); 2307 }, 2308 initializeImageUpload: function(imageInput) { 2309 initializeImageUpload(imageInput); 2310 }, 2311 getSelectedFiles: function () { 2312 return selectedFiles; 2313 }, 2314 setupDragAndDrop: setupDragAndDrop, 2315 handleRemoveImage: handleRemoveImage, 2316 setExistingAudio: setExistingAudio 2317 }; 2318})(); 2319 2320// Main initialization 2321window.addEventListener("load", function () { 2322 // Only proceed if we have a map element 2323 if (!document.getElementById(map_el)) { 2324 return; 2325 } 2326 2327 // Restore the extended L object 2328 window.L = window.OUMLeaflet.L; 2329 2330 // Initialize map and get instance 2331 const mapInstance = OUMMap.init(map_el); 2332 2333 // Initialize markers 2334 const markersModule = OUMMarkers.init(mapInstance); 2335 2336 // Add markers from the global oum_all_locations 2337 if ( 2338 typeof oum_all_locations !== "undefined" && 2339 Array.isArray(oum_all_locations) 2340 ) { 2341 markersModule.addMarkers(oum_all_locations); 2342 } 2343 2344 // Initialize location form 2345 OUMFormMap.init(mapInstance); 2346 2347 // Initialize form controller 2348 OUMFormController.init(); 2349 2350 // Initialize media handling 2351 OUMMedia.init(); 2352 2353 // Setup filter events 2354 const markerFilterInput = document.getElementById("oum_filter_markers"); 2355 if (markerFilterInput) { 2356 markerFilterInput.addEventListener("input", OUMMarkers.filterMarkers); 2357 } 2358 2359 const categoryInputs = document.querySelectorAll(
2360 '.open-user-map .oum-filter-controls [name="type"]' 2361 ); 2362 if (categoryInputs.length > 0) { 2363 categoryInputs.forEach((input) => { 2364 input.addEventListener("change", OUMMarkers.filterMarkers); 2365 }); 2366 } 2367 2368 // Execute custom JS from settings 2369 if (typeof custom_js !== "undefined" && custom_js.snippet) { 2370 try { 2371 // Wrap custom JS execution in a try-catch with proper element existence checks 2372 const wrappedJS = ` 2373 try { 2374 if (typeof document !== 'undefined') { 2375 // Defer map2-related code execution 2376 if (${custom_js.snippet.includes("oumMap2")}) { 2377 // Create a MutationObserver to watch for the form map initialization 2378 const observer = new MutationObserver((mutations) => { 2379 if (window.oumMap2) { 2380 // Execute the custom JS only when oumMap2 is available 2381 try { 2382 ${custom_js.snippet} 2383 } catch (e) { 2384 console.warn('Custom JS execution error (deferred):', e); 2385 } 2386 observer.disconnect(); 2387 } 2388 }); 2389 2390 // Start observing the document for the form map to be added 2391 observer.observe(document.body, { 2392 childList: true, 2393 subtree: true 2394 }); 2395 } else { 2396 // Execute non-map2 related code immediately 2397 ${custom_js.snippet} 2398 } 2399 } 2400 } catch (e) { 2401 console.warn('Custom JS execution error:', e); 2402 } 2403 `; 2404 Function(wrappedJS)(); 2405 } catch (error) { 2406 console.warn("Error executing custom JS:", error); 2407 } 2408 } 2409});
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.