PageSourceSearch

https://prosperofest.events/wp-content/plugins/open-user-map/src/js/frontend-block-map.js?ver=1.4.4

js prosperofest.events collected 2026-10-10 01:37:58 UTC 75,303 bytes, 2,409 lines download raw bytes

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">&times;</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">&times;</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.