PageSourceSearch

https://www.helsinkiagency.fi/wp-content/plugins/gmap-embed/public/assets/js/wgm-frontend.js?ver=1783699941

js helsinkiagency.fi collected 2026-10-02 10:30:44 UTC 43,672 bytes, 1,248 lines download raw bytes

1/**
2 * WP Google Map Frontend Logic
3 * Centralized script to handle map initialization, search, and directions.
4 * @since 1.9.1
5 */
6
7(function ($) {
8  "use strict";
9
10  window.wgm_front = {
11    instances: {},
12
13    init: function (data) {
14      var self = this;
15      var id = data.count;
16      this.instances[id] = data;
17
18      // Define legacy config early for themes/templates
19      window["wgm_config_" + id] = data.config;
20
21      $(document).ready(function () {
22        self.initMap(id);
23        self.initLightbox(id);
24        self.initSelect2();
25      });
26    },
27
28    /**
29     * Escape HTML strings to prevent XSS
30     * @param {string} str
31     * @returns {string}
32     */
33    escapeHTML: function (str) {
34      if (!str) return "";
35      return String(str)
36        .replace(/&/g, "&")
37        .replace(/</g, "&lt;")
38        .replace(/>/g, "&gt;")
39        .replace(/"/g, "&quot;")
40        .replace(/'/g, "&#039;");
41    },
42
43    /**
44     * Remove theme-injected classes that might interfere with design
45     * @param {string} selector
46     * @param {string[]} allowed
47     */
48    sanitizeClasses: function (selector, allowed) {
49      $(selector).each(function () {
50        var $el = $(this);
51        var classes = ($el.attr("class") || "").split(/\s+/);
52        var filtered = classes.filter(function (c) {
53          return (
54            allowed.indexOf(c) !== -1 ||
55            (c && c.indexOf("wgm-") === 0) ||
56            (c && c.indexOf("wpgmap_") === 0)
57          );
58        });
59        $el.attr("class", filtered.join(" "));
60      });
61    },
62
63    initSelect2: function () {
64      if ($.fn.select2) {
65        $(".wgm-select2").each(function () {
66          var $this = $(this);
67          if (!$this.hasClass("select2-hidden-accessible")) {
68            $this.select2({
69              allowClear: true,
70              width: "100%",
71            });
72          }
73        });
74      }
75    },
76
77    initMap: function (id) {
78      var data = this.instances[id];
79      var self = this;
80
81      var initFn = function () {
82        console.log("WGM: Initializing map " + id);
83        if (typeof google !== "object" || typeof google.maps !== "object") {
84          console.warn("WGM: Google Maps API not ready yet inside initFn.");
85          return;
86        }
87
88        var mapDiv = document.getElementById("srm_gmp_embed_" + id);
89        if (!mapDiv) {
90          console.error("WGM: Map container not found: srm_gmp_embed_" + id);
91          return;
92        }
93
94        console.log(
95          "WGM: Found map container, proceeding with initialization."
96        );
97        try {
98          var mapOptions = {
99            center: new google.maps.LatLng(
100              data.map_center.lat,
101              data.map_center.lng
102            ),
103            zoom: parseInt(data.map_zoom),
104            mapTypeId: google.maps.MapTypeId[data.map_type],
105            scrollwheel: data.options.disable_mouse_wheel_zoom !== "Y",
106            zoomControl: data.options.zoom_control,
107            zoomControlOptions: {
108              position:
109                google.maps.ControlPosition[data.options.zoom_control_pos],
110            },
111            mapTypeControl: data.options.map_type_control,
112            mapTypeControlOptions: {
113              position:
114                google.maps.ControlPosition[data.options.map_type_control_pos],
115            },
116            streetViewControl: data.options.street_view_control,
117            streetViewControlOptions: {
118              position:
119                google.maps.ControlPosition[
120                  data.options.street_view_control_pos
121                ],
122            },
123            fullscreenControl: data.options.fullscreen_control,
124            fullscreenControlOptions: {
125              position:
126                google.maps.ControlPosition[
127                  data.options.fullscreen_control_pos
128                ],
129            },
130            rotateControl: data.options.rotate_control,
131            rotateControlOptions: {
132              position:
133                google.maps.ControlPosition[data.options.rotate_control_pos],
134            },
135            scaleControl: data.options.scale_control,
136            scaleControlOptions: {
137              position:
138                google.maps.ControlPosition[data.options.scale_control_pos],
139            },
140            draggable: data.options.disable_mouse_dragging !== "Y",
141            disableDoubleClickZoom:
142              data.options.disable_mouse_double_click_zooming === "Y",
143          };
144
145          var map = new google.maps.Map(mapDiv, mapOptions);
146          data.map = map;
147          window["wgm_map_" + id] = map; // For legacy/theme compatibility
148
149          // Apply Theme
150          if (data.theme_json) {
151            try {
152              map.setOptions({ styles: JSON.parse(data.theme_json) });
153            } catch (e) {
154              console.error("WGM: Invalid map theme JSON:", e);
155            }
156          }
157
158          // Load Markers
159          self.loadMarkers(id);
160
161          // Initialize Search and Directions once map is ready
162          self.initSearch(id);
163          self.initDirections(id);
164
165          // Trigger hooks for themes or other extensions
166          $(window).trigger("wgm_map_init_" + id, { map: map, data: data });
167        } catch (e) {
168          console.error("WGM: Error during map initialization:", e);
169        }
170      };
171
172      if (typeof google === "object" && typeof google.maps === "object") {
173        console.log("WGM: Google Maps API ready, initializing immediately.");
174        initFn();
175      } else {
176        console.log("WGM: Google Maps API not ready, queuing initialization.");
177        window.wgm_map_queue = window.wgm_map_queue || [];
178        window.wgm_map_queue.push(initFn);
179      }
180    },
181
182    loadMarkers: function (id) {
183      var data = this.instances[id];
184      var self = this;
185
186      var ajaxData = {
187        action: "wpgmapembed_p_get_markers_by_map_id",
188        _wgm_p_nonce: data.nonces.marker_render,
189        data: {
190          map_id: data.map_id,
191        },
192      };
193
194      $.post(data.ajax_url, ajaxData, function (response) {
195        try {
196          response =
197            typeof response === "string" ? JSON.parse(response) : response;
198        } catch (e) {
199          console.error("WGM: Failed to parse markers response", e);
200          return;
201        }
202
203        if (response.markers && response.markers.length > 0) {
204          if (!window.custom_markers) window.custom_markers = {};
205          if (!window.custom_marker_infowindows)
206            window.custom_marker_infowindows = {};
207          window.custom_markers[id] = [];
208          window.custom_marker_infowindows[id] = [];
209
210          // Legacy config global for themes
211          window["wgm_config_" + id] = data.config;
212
213          response.markers.forEach(function (markerData, i) {
214            markerData.marker_idx = i;
215            self.addMarker(id, markerData, i);
216          });
217
218          // Trigger event for themes to consume markers
219          $(window).trigger("wgm_markers_loaded_" + id, {
220            markers: response.markers,
221          });
222
223          // Search pre-fill logic if single marker
224          if (
225            response.markers.length === 1 &&
226            data.config.enable_modern_direction
227          ) {
228            var singleAddr = response.markers[0].marker_name || "";
229            $("#srm_gmap_to_" + id).val(
230              self.escapeHTML(singleAddr.replace(/(<([^>]+)>)/gi, ""))
231            );
232          }
233        }
234      }).fail(function (xhr, status, error) {
235        console.error("WGM: Failed to load markers: " + error);
236      });
237    },
238
239    addMarker: function (id, markerData, index) {
240      var data = this.instances[id];
241      var self = this;
242      var latlng = (markerData.lat_lng || "0,0").split(",");
243      if (latlng.length < 2) {
244        console.warn("WGM: Invalid coordinates for marker", markerData);
245        return;
246      }
247
248      var anim = null;
249      if (markerData.animation === "BOUNCE")
250        anim = google.maps.Animation.BOUNCE;
251      if (markerData.animation === "DROP") anim = google.maps.Animation.DROP;
252
253      var marker = new google.maps.Marker({
254        position: new google.maps.LatLng(
255          parseFloat(latlng[0]),
256          parseFloat(latlng[1])
257        ),
258        title: self.escapeHTML(markerData.marker_name),
259        animation: anim,
260        icon: markerData.icon === "" ? data.default_icon : markerData.icon,
261        visible: !data.config.hide_markers,
262        map: data.map,
263      });
264
265      marker.wgm_data = markerData;
266      window.custom_markers[id][index] = marker;
267
268      // Infowindow preparation
269      var infowindow = new google.maps.InfoWindow({
270        content: self.getInfoWindowContent(id, markerData),
271        maxWidth: 320,
272      });
273      window.custom_marker_infowindows[id][index] = infowindow;
274
275      // Auto-open infowindow if configured
276      if (markerData.show_desc_by_default === "1") {
277        self.closeAllInfoWindows(id);
278        infowindow.open({ anchor: marker, shouldFocus: false });
279      }
280
281      // Handle marker click behavior
282      if (markerData.have_marker_link === "1") {
283        // If marker has a link, navigate to it on click
284        marker.addListener("click", function () {
285          var target =
286            markerData.marker_link_new_tab === "1" ? "_blank" : "_self";
287          window.open(markerData.marker_link, target);
288        });
289      } else {
290        // If no link, always open infowindow on click (regardless of show_desc_by_default)
291        marker.addListener("click", function () {
292          self.closeAllInfoWindows(id);
293          infowindow.open({ anchor: marker, shouldFocus: false });
294        });
295      }
296    },
297
298    getInfoWindowContent: function (id, markerData) {
299      var data = this.instances[id];
300      var self = this;
301      var hasImg = markerData.marker_image && markerData.marker_image !== "";
302      if (data.marker_listing_type === "basic_list") hasImg = false;
303
304      var imgHtml = hasImg
305        ? '<div class="wgm-iw-img-wrap"><img src="' +
306          markerData.marker_image +
307          '" class="wgm-infowindow-img wgm-lightbox-trigger" /></div>'
308        : "";
309      var titleHtml = markerData.marker_name
310        ? '<p class="info_content_title wgm-infowindow-title">' +
311          self.escapeHTML(markerData.marker_name) +
312          "</p>"
313        : "";
314
315      var dirLink = "";
316      if (data.config.enable_direction_link) {
317        var dirAddr = (markerData.address || markerData.lat_lng).replace(
318          /'/g,
319          "&#39;"
320        );
321        dirLink =
322          '<br><a href="#" class="wgm-get-dir-link wgm-dir-link-' +
323          id +
324          '" data-addr="' +
325          dirAddr +
326          '" style="display:inline-block;margin-top:5px;font-size:13px;">' +
327          data.i18n.get_directions +
328          "</a>";
329      }
330
331      var wrapperClass =
332        "wgm-infowindow-wrapper" + (hasImg ? " wgm-has-img" : " wgm-no-img");
333      return (
334        '<div class="' +
335        wrapperClass +
336        '">' +
337        imgHtml +
338        '<div class="wgm-infowindow-content">' +
339        titleHtml +
340        '<div class="wgm-iw-desc">' +
341        markerData.marker_desc +
342        "</div>" +
343        dirLink +
344        "</div>" +
345        "</div>"
346      );
347    },
348
349    closeAllInfoWindows: function (id) {
350      if (
351        window.custom_marker_infowindows &&
352        window.custom_marker_infowindows[id]
353      ) {
354        window.custom_marker_infowindows[id].forEach(function (iw) {
355          if (iw) iw.close();
356        });
357      }
358    },
359
360    initSearch: function (id) {
361      var data = this.instances[id];
362      var self = this;
363      var controlDiv = document.getElementById("wpgmap_search_control_" + id);
364      var mapWrapper = document.getElementById("srm_gmp_embed_" + id);
365
366      if (!controlDiv || !mapWrapper) return;
367
368      // Sanitize classes to remove theme injection
369      self.sanitizeClasses(controlDiv, ["wgm-search-control-container"]);
370      var $cd = $(controlDiv);
371      self.sanitizeClasses($cd.find(".wpgmap_search_box"), [
372        "wpgmap_search_box",
373      ]);
374      self.sanitizeClasses($cd.find(".wpgmap_search_input"), [
375        "wpgmap_search_input",
376      ]);
377      self.sanitizeClasses($cd.find(".wpgmap_search_select"), [
378        "wpgmap_search_select",
379      ]);
380      self.sanitizeClasses($cd.find(".wpgmap_search_btn"), [
381        "wpgmap_search_btn",
382      ]);
383      self.sanitizeClasses($cd.find(".wpgmap_search_clear"), [
384        "wpgmap_search_clear",
385      ]);
386
387      // Placement logic
388      if (data.config.placement === "above_map") {
389        $(mapWrapper).before(controlDiv);
390        $(controlDiv).show();
391      } else if (data.config.placement === "below_map") {
392        $(mapWrapper).after(controlDiv);
393        $(controlDiv).show();
394      } else {
395        data.map.controls[google.maps.ControlPosition.TOP_CENTER].push(
396          controlDiv
397        );
398        $(controlDiv).show();
399      }
400
401      var btn = $("#wpgmap_search_btn_" + id);
402      var locInput = $("#wpgmap_search_loc_" + id);
403      var keyInput = $("#wpgmap_search_key_" + id);
404      var radSelect = $("#wpgmap_search_radius_" + id);
405
406      var catInput = $("#wpgmap_search_cat_" + id);
407      var catPanel = $("#wpgmap_filter_panel_" + id);
408      var catToggle = $("#wpgmap_filter_toggle_" + id);
409
410      var spinner = $("#wpgmap_search_spinner_" + id);
411      var notFoundMsg = $("#wpgmap_not_found_msg_" + id);
412
413      // Bind Close Event Globally for this instance
414      notFoundMsg.find(".wgm-not-found-close").on("click", function (e) {
415        e.preventDefault();
416        notFoundMsg.stop(true, false).animate({ opacity: 0 }, 500, function () {
417          $(this).removeClass("wgm-show").css("opacity", "");
418        });
419        if (window["wgm_not_found_timer_" + id]) {
420          clearTimeout(window["wgm_not_found_timer_" + id]);
421        }
422      });
423      var searchCircle = null;
424
425      var adjustWidth = function () {
426        var container = $(mapWrapper).closest(".wgm-map-listing-container");
427        var containerWidth = container.width();
428        var mapWidth = mapWrapper.offsetWidth;
429
430        // Adjust input visibility based on map width
431        var container = $(mapWrapper).closest(".wgm-map-listing-container");
432        var containerWidth = container.width();
433        var mapWidth = mapWrapper.offsetWidth;
434
435        // Container-Aware Stacking Logic
436        if (containerWidth < 600) {
437          container.addClass("wgm-force-stack");
438        } else {
439          container.removeClass("wgm-force-stack");
440        }
441
442        if (mapWidth < 480) {
443          if (keyInput.length) keyInput.hide();
444        } else {
445          if (keyInput.length) keyInput.show();
446        }
447        if (mapWidth < 380) {
448          if (radSelect.length) radSelect.hide();
449          if (catToggle.length) catToggle.hide();
450        } else {
451          if (radSelect.length) radSelect.show();
452          if (catToggle.length) catToggle.show();
453        }
454      };
455
456      adjustWidth();
457
458      // Use ResizeObserver if available for better performance and theme compatibility
459      if (typeof ResizeObserver !== "undefined") {
460        var ro = new ResizeObserver(function () {
461          adjustWidth();
462        });
463        ro.observe(mapWrapper);
464      } else {
465        $(window).on("resize", adjustWidth);
466      }
467
468      google.maps.event.addListenerOnce(data.map, "idle", adjustWidth);
469
470      if (locInput.length && typeof google.maps.places !== "undefined") {
471        var autocomplete = new google.maps.places.Autocomplete(locInput[0]);
472        autocomplete.bindTo("bounds", data.map);
473      }
474
475      var getDistance = function (lat1, lon1, lat2, lon2) {
476        var R = 6371; // km
477        var dLat = ((lat2 - lat1) * Math.PI) / 180;
478        var dLon = ((lon2 - lon1) * Math.PI) / 180;
479        var a =
480          Math.sin(dLat / 2) * Math.sin(dLat / 2) +
481          Math.cos((lat1 * Math.PI) / 180) *
482            Math.cos((lat2 * Math.PI) / 180) *
483            Math.sin(dLon / 2) *
484            Math.sin(dLon / 2);
485        return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
486      };
487
488      var updateMapAndList = function (
489        centerLat,
490        centerLng,
491        radius,
492        keyword,
493        catId
494      ) {
495        var markers = window.custom_markers[id] || [];
496        var filteredData = [];
497        var bounds = new google.maps.LatLngBounds();
498        var hasVisible = false;
499        var hasLocation = centerLat !== null && centerLng !== null;
500
501        if (searchCircle) searchCircle.setMap(null);
502        if (window["wgm_center_marker_" + id])
503          window["wgm_center_marker_" + id].setMap(null);
504
505        if (hasLocation) {
506          if (radius) {
507            searchCircle = new google.maps.Circle({
508              strokeColor: data.circle.stroke_color,
509              strokeOpacity: data.circle.stroke_opacity,
510              strokeWeight: data.circle.stroke_weight,
511              fillColor: data.circle.fill_color,
512              fillOpacity: data.circle.fill_opacity,
513              map: data.map,
514              center: { lat: centerLat, lng: centerLng },
515              radius: radius * 1000,
516            });
517          }
518          if (data.config.show_center_icon) {
519            window["wgm_center_marker_" + id] = new google.maps.Marker({
520              position: { lat: centerLat, lng: centerLng },
521              map: data.map,
522              title: "Search Center",
523              icon: "https://maps.google.com/mapfiles/ms/icons/blue-dot.png",
524            });
525          }
526        }
527
528        markers.forEach(function (m, i) {
529          var d = Object.assign({}, m.wgm_data);
530          d.marker_idx = i;
531          var isVisible = true;
532          var distVal = 0;
533
534          if (hasLocation) {
535            var mLatLng = m.getPosition();
536            var distKm = getDistance(
537              centerLat,
538              centerLng,
539              mLatLng.lat(),
540              mLatLng.lng()
541            );
542            if (radius && distKm > radius) isVisible = false;
543
544            if (isVisible && data.config.show_distance) {
545              distVal =
546                data.config.distance_unit === "miles"
547                  ? distKm * 0.621371
548                  : distKm;
549              var unit = data.config.distance_unit === "miles" ? "miles" : "km";
550              d.distance = distVal.toFixed(2) + " " + unit;
551              d.distance_html =
552                '<span class="wgm-distance-badge">' + d.distance + "</span>";
553            }
554          }
555
556          if (isVisible && keyword) {
557            var hay = (
558              (d.marker_name || "") +
559              " " +
560              (d.marker_desc || "") +
561              " " +
562              (d.address || "")
563            ).toLowerCase();
564            if (hay.indexOf(keyword) === -1) isVisible = false;
565          }
566
567          if (isVisible && catId && catId.length > 0) {
568            var markerCats = d.category_id
569              ? d.category_id.toString().split(",")
570              : [];
571            var selectedCats = Array.isArray(catId)
572              ? catId
573              : [catId.toString()];
574            var logic = data.config.category_selection_logic || "OR";
575
576            if (logic === "AND") {
577              // Show if marker has ALL selected categories
578              for (var j = 0; j < selectedCats.length; j++) {
579                if (markerCats.indexOf(selectedCats[j].toString()) === -1) {
580                  isVisible = false;
581                  break;
582                }
583              }
584            } else {
585              // OR logic: Show if marker has ANY of the selected categories
586              var found = false;
587              for (var k = 0; k < selectedCats.length; k++) {
588                if (markerCats.indexOf(selectedCats[k].toString()) !== -1) {
589                  found = true;
590                  break;
591                }
592              }
593              if (!found) isVisible = false;
594            }
595          }
596
597          m.setVisible(isVisible);
598          if (isVisible) {
599            filteredData.push(d);
600            bounds.extend(m.getPosition());
601            hasVisible = true;
602          }
603        });
604
605        if (hasLocation && data.config.sort_by_distance) {
606          filteredData.sort(function (a, b) {
607            return (
608              (parseFloat(a.distance) || 0) - (parseFloat(b.distance) || 0)
609            );
610          });
611        }
612
613        $(window).trigger("wgm_search_update_" + id, { markers: filteredData });
614
615        if (hasVisible) {
616          notFoundMsg.removeClass("wgm-show");
617          if (hasLocation && radius) {
618            data.map.fitBounds(searchCircle.getBounds());
619          } else {
620            data.map.fitBounds(bounds);
621            if (data.map.getZoom() > 15) data.map.setZoom(15);
622          }
623        } else {
624          notFoundMsg.addClass("wgm-show");
625
626          // Clear any existing timeout
627          if (window["wgm_not_found_timer_" + id]) {
628            clearTimeout(window["wgm_not_found_timer_" + id]);
629          }
630
631          // Set 8s auto-hide timer
632          window["wgm_not_found_timer_" + id] = setTimeout(function () {
633            notFoundMsg
634              .stop(true, false)
635              .animate({ opacity: 0 }, 500, function () {
636                $(this).removeClass("wgm-show").css("opacity", "");
637              });
638          }, 8000);
639
640          if (hasLocation) {
641            data.map.setCenter({ lat: centerLat, lng: centerLng });
642            data.map.setZoom(10);
643          }
644        }
645      };
646
647      var executeSearch = function () {
648        var loc = locInput.length ? locInput.val().trim() : "";
649        var key = keyInput.length ? keyInput.val().trim().toLowerCase() : "";
650        var rad =
651          radSelect.length && radSelect.val()
652            ? parseFloat(radSelect.val())
653            : null;
654        var cat =
655          catInput.length && catInput.val()
656            ? catInput.val().split(",") // Pass array of strings
657            : null;
658
659        if (loc) {
660          spinner.show();
661          var geocoder = new google.maps.Geocoder();
662          geocoder.geocode({ address: loc }, function (results, status) {
663            spinner.hide();
664            if (status === "OK") {
665              var pos = results[0].geometry.location;
666              updateMapAndList(pos.lat(), pos.lng(), rad, key, cat);
667            } else {
668              alert("Geocode was not successful: " + status);
669            }
670          });
671        } else {
672          updateMapAndList(null, null, null, key, cat);
673        }
674      };
675
676      btn.on("click", executeSearch);
677
678      // Clear button functionality
679      var clearBtn = $("#wpgmap_search_clear_" + id);
680
681      // Show/hide clear button based on input values
682      var toggleClearBtn = function () {
683        var hasLocation = locInput.length && locInput.val().trim() !== "";
684        var hasKeyword = keyInput.length && keyInput.val().trim() !== "";
685        var hasRadius = radSelect.length && radSelect.val() !== "";
686        var hasCategory = catInput.length && catInput.val() !== "";
687
688        if (hasLocation || hasKeyword || hasRadius || hasCategory) {
689          clearBtn.show();
690        } else {
691          clearBtn.hide();
692        }
693      };
694
695      // Monitor input changes
696      if (locInput.length) locInput.on("input", toggleClearBtn);
697      if (keyInput.length) keyInput.on("input", toggleClearBtn);
698      if (radSelect.length) radSelect.on("change", toggleClearBtn);
699      if (catInput.length) catInput.on("change", toggleClearBtn);
700
701      // Check initial values (for pre-loaded defaults)
702      toggleClearBtn();
703
704      // Clear button click handler
705      clearBtn.on("click", function () {
706        // Clear all inputs
707        if (locInput.length) locInput.val("");
708        if (keyInput.length) keyInput.val("");
709        if (radSelect.length) radSelect.val("");
710        if (catInput.length) catInput.val("");
711        if (catPanel.length)
712          catPanel.find(".wgm-front-cat-checkbox").prop("checked", false);
713
714        // Hide clear button
715        clearBtn.hide();
716
717        // Remove search circle and center marker
718        if (searchCircle) searchCircle.setMap(null);
719        if (window["wgm_center_marker_" + id])
720          window["wgm_center_marker_" + id].setMap(null);
721
722        // Reset all markers to visible
723        var markers = window.custom_markers[id] || [];
724        markers.forEach(function (m) {
725          m.setVisible(true);
726        });
727
728        // Hide not found message
729        notFoundMsg.removeClass("wgm-show");
730
731        // Trigger listing update with all markers
732        var allMarkers = markers.map(function (m, i) {
733          var d = Object.assign({}, m.wgm_data);
734          d.marker_idx = i;
735          return d;
736        });
737        $(window).trigger("wgm_search_update_" + id, { markers: allMarkers });
738
739        // Fit bounds to show all markers
740        if (markers.length > 0) {
741          var bounds = new google.maps.LatLngBounds();
742          markers.forEach(function (m) {
743            bounds.extend(m.getPosition());
744          });
745          data.map.fitBounds(bounds);
746        }
747      });
748
749      var triggerSearch = function (e) {
750        if (e.key === "Enter") executeSearch();
751      };
752      locInput.on("keydown", triggerSearch);
753      if (keyInput.length) keyInput.on("keydown", triggerSearch);
754
755      // Category Tree Builder
756      if (data.categories && catPanel.length) {
757        var buildCategoryTree = function (parentId) {
758          var html = "";
759          var children = data.categories.filter(function (cat) {
760            return cat.parent_id == parentId;
761          });
762
763          if (children.length > 0) {
764            if (parentId != 0) {
765              html += '<ul class="wgm-filter-sublist">';
766            }
767            children.forEach(function (cat) {
768              html += '<li class="wgm-filter-item">';
769              html += "<label>";
770              html +=
771                '<input type="checkbox" class="wgm-front-cat-checkbox" value="' +
772                self.escapeHTML(cat.id) +
773                '"> ';
774              html +=
775                '<span class="wgm-cat-name">' +
776                self.escapeHTML(cat.name) +
777                "</span>";
778              html += "</label>";
779              html += buildCategoryTree(cat.id);
780              html += "</li>";
781            });
782            if (parentId != 0) {
783              html += "</ul>";
784            }
785          }
786          return html;
787        };
788
789        var listHtml =
790          '<div class="wgm-filter-header">' +
791          '<span class="wgm-filter-close" title="Close">&times;</span>' +
792          "</div>" +
793          '<ul class="wgm-filter-list">' +
794          buildCategoryTree(0) +
795          "</ul>";
796
797        if (data.categories.length === 0)
798          listHtml = '<p class="wgm-no-cats">No categories</p>';
799        catPanel.html(listHtml);
800
801        // Toggle Panel
802        catToggle.on("click", function (e) {
803          e.preventDefault();
804          e.stopPropagation();
805          catPanel.toggleClass("hidden");
806        });
807
808        // Prevent closing when clicking inside panel
809        catPanel.on("click", function (e) {
810          e.stopPropagation();
811        });
812
813        // Close Icon Click
814        catPanel.on("click", ".wgm-filter-close", function (e) {
815          e.preventDefault();
816          e.stopPropagation();
817          catPanel.addClass("hidden");
818        });
819
820        // Close on click outside
821        // Prevent closing when clicking on labels/inputs specifically (extra safety)
822        catPanel.on("click", "label, input", function (e) {
823          e.stopPropagation();
824        });
825
826        // Close on click outside
827        $(document).on("click", function (e) {
828          if (
829            $(e.target).closest("#wpgmap_filter_panel_" + id).length === 0 &&
830            $(e.target).closest("#wpgmap_filter_toggle_" + id).length === 0
831          ) {
832            catPanel.addClass("hidden");
833          }
834        });
835
836        // Handle Checkbox Change ->
836 Update Hidden Input -> Trigger Search
837        catPanel.on("change", ".wgm-front-cat-checkbox", function () {
838          var $this = $(this);
839          var isChecked = $this.prop("checked");
840          var val = $this.val();
841
842          // Cascading Logic: Check/Uncheck all children
843          // 1. Find the parent <li> of the changed checkbox
844          var $parentLi = $this.closest("li");
845          // 2. Find any nested <ul> which contains children
846          var $childrenList = $parentLi.find("ul");
847          // 3. Find all checkboxes within that list and update them
848          if ($childrenList.length) {
849            $childrenList
850              .find(".wgm-front-cat-checkbox")
851              .prop("checked", isChecked);
852          }
853
854          var selectedIds = [];
855          catPanel.find(".wgm-front-cat-checkbox:checked").each(function () {
856            selectedIds.push($(this).val());
857          });
858          catInput.val(selectedIds.join(","));
859          executeSearch(); // Trigger search/filter
860        });
861      }
862
863      // Check initial values (for pre-loaded defaults)
864      if (catInput.length && catInput.val()) {
865        var preselected = catInput.val().split(",");
866        preselected.forEach(function (pid) {
867          catPanel
868            .find('.wgm-front-cat-checkbox[value="' + pid + '"]')
869            .prop("checked", true);
870        });
871        executeSearch();
872      }
873    },
874
875    initDirections: function (id) {
876      var data = this.instances[id];
877      var self = this;
878      var drawer = $("#wgm_direction_drawer_" + id);
879      var legacyFromInput = $("#srm_gmap_from_" + id);
880      var legacyToInput = $("#srm_gmap_to_" + id);
881
882      // Define the open directions function for both modern drawer and legacy box
883      window["wgm_open_directions_" + id] = function (destination) {
884        if (drawer.length) {
885          // Modern drawer exists
886          var toInput = $("#wgm_dir_to_" + id);
887          var fromInput = $("#wgm_dir_from_" + id);
888          var toggleBtn = $("#wgm_drawer_toggle_" + id);
889          toInput.val(destination);
890          drawer.addClass("active");
891          toggleBtn.fadeOut(200);
892          fromInput.focus();
893        } else if (legacyToInput.length) {
894          // Legacy direction box exists
895          legacyToInput.val(destination);
896          legacyFromInput.focus();
897          // Scroll to the legacy direction box
898          $("html, body").animate(
899            {
900              scrollTop:
901                legacyToInput.closest(".wgm-legacy-direction-box").offset()
902                  .top - 20,
903            },
904            500
905          );
906        }
907      };
908
909      // Delegate events from infowindow and listing
910      $(document).on("click", ".wgm-dir-link-" + id, function (e) {
911        e.preventDefault();
912        self.closeAllInfoWindows(id);
913        window["wgm_open_directions_" + id]($(this).data("addr"));
914      });
915
916      $(document).on(
917        "click",
918        "#wgm_listing_area_" + id + " .wgm-get-direction-btn",
919        function (e) {
920          e.preventDefault();
921          var idx = $(this).data("idx");
922          var m = window.custom_markers[id][idx];
923          if (m)
924            window["wgm_open_directions_" + id](
925              m.wgm_data.address || m.wgm_data.lat_lng
926            );
927        }
928      );
929
930      // Modern drawer initialization
931      if (!drawer.length) {
932        // No modern drawer, skip to legacy directions
933        this.initLegacyDirections(id);
934        return;
935      }
936
937      var toggleBtn = $("#wgm_drawer_toggle_" + id);
938      var fromInput = $("#wgm_dir_from_" + id);
939      var toInput = $("#wgm_dir_to_" + id);
940      var goBtn = drawer.find(".wgm-dir-go-btn");
941      var travelMode = "DRIVING";
942      var resultsPanel = $("#wgm_dir_results_" + id);
943
944      var directionsService = new google.maps.DirectionsService();
945      var directionsRenderer = new google.maps.DirectionsRenderer({
946        map: data.map,
947        suppressMarkers: false,
948        panel: resultsPanel[0],
949      });
950
951      toggleBtn.on("click", function () {
952        drawer.addClass("active");
953        toggleBtn.fadeOut(200);
954      });
955      drawer.find(".wgm-drawer-close").on("click", function () {
956        drawer.removeClass("active");
957        toggleBtn.fadeIn(200);
958        directionsRenderer.setDirections({ routes: [] });
959        resultsPanel.empty().removeClass("active");
960      });
961
962      if (typeof google.maps.places !== "undefined") {
963        new google.maps.places.Autocomplete(fromInput[0]).bindTo(
964          "bounds",
965          data.map
966        );
967        new google.maps.places.Autocomplete(toInput[0]).bindTo(
968          "bounds",
969          data.map
970        );
971      }
972
973      drawer.find(".wgm-travel-btn").on("click", function () {
974        drawer.find(".wgm-travel-btn").removeClass("active");
975        $(this).addClass("active");
976        travelMode = $(this).data("mode");
977      });
978
979      drawer.find(".wgm-dir-adv-toggle").on("click", function () {
980        drawer.find(".wgm-dir-adv-options").toggleClass("active");
981      });
982
983      var waypointsContainer = $("#wgm_waypoints_container_" + id);
984      $("#wgm_add_waypoint_" + id).on("click", function () {
985        var wpId = "wgm_wp_" + Date.now();
986        var html =
987          '<div class="wgm-dir-input-group wgm-waypoint-item" id="' +
988          wpId +
989          '">' +
990          '<div class="wgm-dir-field-wrap">' +
991          '<input type="text" class="wgm-dir-input wgm-waypoint-input" placeholder="' +
992          data.i18n.via +
993          '">' +
994          '<button type="button" class="wgm-remove-waypoint-btn" title="' +
995          data.i18n.remove_waypoint +
996          '">' +
997          '<svg viewBox="0 0 24 24" width="16" height="16"><path fill="currentColor" d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path></svg>' +
998          "</button>" +
999          "</div>" +
1000          "</div>";
1001        waypointsContainer.append(html);
1002        if (typeof google.maps.places !== "undefined") {
1003          new google.maps.places.Autocomplete(
1004            waypointsContainer.find("#" + wpId + " input")[0]
1005          ).bindTo("bounds", data.map);
1006        }
1007        $("#" + wpId + " .wgm-remove-waypoint-btn").on("click", function () {
1008          $("#" + wpId).remove();
1009        });
1010      });
1011
1012      drawer.find(".wgm-current-loc-btn").on("click", function () {
1013        var btn = $(this);
1014        var targetInput = btn.siblings("input");
1015        if (navigator.geolocation) {
1016          btn.css("opacity", "0.5");
1017          navigator.geolocation.getCurrentPosition(
1018            function (position) {
1019              btn.css("opacity", "1");
1020              var geocoder = new google.maps.Geocoder();
1021              geocoder.geocode(
1022                {
1023                  location: {
1024                    lat: position.coords.latitude,
1025                    lng: position.coords.longitude,
1026                  },
1027                },
1028                function (results, status) {
1029                  if (status === "OK" && results[0])
1030                    targetInput.val(results[0].formatted_address);
1031                  else
1032                    targetInput.val(
1033                      position.coords.latitude + "," + position.coords.longitude
1034                    );
1035                }
1036              );
1037            },
1038            function (error) {
1039              btn.css("opacity", "1");
1040              console.error("Geolocation error:", error);
1041              alert(
1042                (data.i18n.geo_fail || "Unable to get your location") +
1043                  ": " +
1044                  error.message
1045              );
1046            }
1047          );
1048        } else {
1049          alert(data.i18n.geo_not_supported);
1050        }
1051      });
1052
1053      goBtn.on("click", function () {
1054        var origin = fromInput.val().trim();
1055        var destination = toInput.val().trim();
1056        var waypoints = [];
1057        drawer.find(".wgm-waypoint-input").each(function () {
1058          var val = $(this).val().trim();
1059          if (val) waypoints.push({ location: val, stopover: true });
1060        });
1061
1062        if (!origin || !destination) {
1063          alert(data.i18n.origin_dest_required);
1064          return;
1065        }
1066
1067        var request = {
1068          origin: origin,
1069          destination: destination,
1070          waypoints: waypoints,
1071          optimizeWaypoints: true,
1072          travelMode: google.maps.TravelMode[travelMode],
1073          avoidTolls: drawer.find(".wgm-avoid-tolls").is(":checked"),
1074          avoidHighways: drawer.find(".wgm-avoid-highways").is(":checked"),
1075          avoidFerries: drawer.find(".wgm-avoid-ferries").is(":checked"),
1076        };
1077
1078        directionsService.route(request, function (result, status) {
1079          if (status == "OK") {
1080            directionsRenderer.setDirections(result);
1081            resultsPanel.addClass("active");
1082          } else {
1083            alert("Directions request failed: " + status);
1084          }
1085        });
1086      });
1087
1088      this.initLegacyDirections(id);
1089    },
1090
1091    initLegacyDirections: function (id) {
1092      var data = this.instances[id];
1093      var legacySubmit = $("#wp_gmap_submit_" + id);
1094      var directionsService = new google.maps.DirectionsService();
1095
1096      if (legacySubmit.length) {
1097        var legacyRenderer = new google.maps.DirectionsRenderer({
1098          map: data.map,
1099          panel: document.getElementById("wp_gmap_directions_" + id),
1100        });
1101
1102        // Initialize Google Places Autocomplete on inputs (if Places API is available)
1103        var fromInput = document.getElementById("srm_gmap_from_" + id);
1104        var toInput = document.getElementById("srm_gmap_to_" + id);
1105
1106        // Enable autocomplete on legacy direction inputs
1107        if (
1108          typeof google.maps.places !== "undefined" &&
1109          data.options.enable_direction_form_auto_complete
1110        ) {
1111          if (fromInput) {
1112            var fromAutocomplete = new google.maps.places.Autocomplete(
1113              fromInput,
1114              {
1115                fields: ["formatted_address", "geometry", "name"],
1116              }
1117            );
1118            fromAutocomplete.bindTo("bounds", data.map);
1119          }
1120
1121          if (toInput) {
1122            var toAutocomplete = new google.maps.places.Autocomplete(toInput, {
1123              fields: ["formatted_address", "geometry", "name"],
1124            });
1125            toAutocomplete.bindTo("bounds", data.map);
1126          }
1127        }
1128
1129        // Travel mode switcher for legacy box
1130        $(".wgm-legacy-travel-btn").on("click", function () {
1131          $(".wgm-legacy-travel-btn").removeClass("active");
1132          $(this).addClass("active");
1133          var mode = $(this).data("mode");
1134          $("#srm_gmap_mode_" + id).val(mode);
1135        });
1136
1137        // Location buttons for legacy box
1138        $(".wgm-legacy-loc-btn").on("click", function () {
1139          var btn = $(this);
1140          var targetInput = btn.siblings("input");
1141          if (navigator.geolocation) {
1142            btn.css("opacity", "0.5");
1143            navigator.geolocation.getCurrentPosition(
1144              function (position) {
1145                btn.css("opacity", "1");
1146                var geocoder = new google.maps.Geocoder();
1147                geocoder.geocode(
1148                  {
1149                    location: {
1150                      lat: position.coords.latitude,
1151                      lng: position.coords.longitude,
1152                    },
1153                  },
1154                  function (results, status) {
1155                    if (status === "OK" && results[0])
1156                      targetInput.val(results[0].formatted_address);
1157                    else
1158                      targetInput.val(
1159                        position.coords.latitude +
1160                          "," +
1161                          position.coords.longitude
1162                      );
1163                  }
1164                );
1165              },
1166              function (error) {
1167                btn.css("opacity", "1");
1168                console.error("Geolocation error:", error);
1169                alert(
1170                  (data.i18n.geo_fail || "Unable to get your location") +
1171                    ": " +
1172                    error.message
1173                );
1174              }
1175            );
1176          } else {
1177            alert(
1178              data.i18n.geo_not_supported || "Geolocation is not supported"
1179            );
1180          }
1181        });
1182
1183        legacySubmit.on("click", function () {
1184          var mode = $("#srm_gmap_mode_" + id).val(),
1185            from = $("#srm_gmap_from_" + id).val(),
1186            to = $("#srm_gmap_to_" + id).val();
1187          if (!from || !to) {
1188            $("#wp_gmap_results_" + id).hide();
1189            return;
1190          }
1191          $("#wp_gmap_loading_" + id).addClass("wgm-show");
1192          directionsService.route(
1193            {
1194              origin: from,
1195              destination: to,
1196              travelMode: google.maps.DirectionsTravelMode[mode],
1197              unitSystem:
1198                data.distance_unit_system === "METRIC"
1199                  ? google.maps.UnitSystem.METRIC
1200                  : google.maps.UnitSystem.IMPERIAL,
1201            },
1202            function (response, status) {
1203              $("#wp_gmap_loading_" + id).removeClass("wgm-show");
1204              if (status === "OK") {
1205                legacyRenderer.setDirections(response);
1206                $("#wp_gmap_results_" + id).show();
1207              } else {
1208                $("#wp_gmap_results_" + id).hide();
1209                alert("Directions request failed: " + status);
1210              }
1211            }
1212          );
1213        });
1214      }
1215    },
1216
1217    initLightbox: function (id) {
1218      var data = this.instances[id];
1219      var overlay = $("#wgm-lightbox-overlay_" + id);
1220      var img = $("#wgm-lightbox-img_" + id);
1221      if (!overlay.length) return;
1222
1223      // Scope to this instance's container to prevent conflicts with multiple maps
1224      var container = $(".wgm-map-id-" + data.map_id).eq(
1225        $(".wgm-map-id-" + data.map_id).index(
1226          $("#srm_gmp_embed_" + id).closest(".wgm-map-id-" + data.map_id)
1227        )
1228      );
1229
1230      // Use instance-specific container instead of document
1231      container.on("click", ".wgm-lightbox-trigger", function (e) {
1232        e.stopPropagation();
1233        img.attr("src", $(this).attr("src"));
1234        overlay.addClass("show");
1235      });
1236
1237      overlay.on("click", function (e) {
1238        if (e.target !== img[0]) overlay.removeClass("show");
1239      });
1240      overlay.find(".wgm-lightbox-close").on("click", function () {
1241        overlay.removeClass("show");
1242      });
1243      $(document).on("keydown", function (e) {
1244        if (e.key === "Escape") overlay.removeClass("show");
1245      });
1246    },
1247  };
1248})(jQuery);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.