PageSourceSearch

https://www.infovodice.com/hr/media/com_easydirectory/js/easydirectory-map.js?1.1.5

js infovodice.com collected 2026-10-02 20:25:30 UTC 24,369 bytes, 695 lines download raw bytes

1/**
2 * Easy Directory: Pro Map Rendering Module
3 * Supports Google Maps & OpenStreetMap Leaflet with Theme-Colored SVG Markers
4 * Full Two-Way Synchronization with Listing Cards (Hover, Click, Pan, Popup)
5 * Copyright (C) 2011 - 2026 JoomlaX. All rights reserved.
6 * License GNU General Public License version 3 or later
7 */
8
9window.EasyDirectory = window.EasyDirectory || {};
10
11(function() {
12  'use strict';
13
14  var activeMapInstance = null;
15  var activeMapType = null; // 'google' or 'leaflet'
16  var activeMarkersMap = {};
17  var activeMarkerDataMap = {};
18  var activeBounds = null;
19  var activeMarkerList = [];
20  var activeInfoWindow = null;
21  var activeRadiusCircle = null;
22  var activeUserMarker = null;
23
24  function getThemeColor() {
25    var root = document.querySelector('.ed-component-wrapper') || document.documentElement;
26    var color = getComputedStyle(root).getPropertyValue('--ed-primary').trim();
27    return color || '#2563eb';
28  }
29
30  function getMarkerSvg(color, isFeatured) {
31    color = color || getThemeColor();
32    var starIcon = isFeatured
33      ? '<path d="M12 6.5l1.6 3.3 3.6.5-2.6 2.5.6 3.6-3.2-1.7-3.2 1.7.6-3.6-2.6-2.5 3.6-.5z" fill="#ffffff"/>'
34      : '<circle cx="12" cy="11" r="4.5" fill="#ffffff"/><circle cx="12" cy="11" r="2.2" fill="' + color + '"/>';
35
36    return '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="46" viewBox="0 0 24 30" style="display: block; transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);">' +
37      '<path d="M12 1C6.48 1 2 5.48 2 11c0 7.5 10 18 10 18s10-10.5 10-18c0-5.52-4.48-10-10-10z" fill="' + color + '" stroke="#ffffff" stroke-width="1.5"/>' +
38      starIcon +
39      '</svg>';
40  }
41
42  function buildPopupHtml(m, themeColor) {
43    var sym = m.currency || '$';
44    var priceHtml = '';
45    if (m.price > 0) {
46      var active = sym.repeat(m.price);
47      var inactive = sym.repeat(Math.max(0, 4 - m.price));
48      priceHtml = '<span class="ed-submeta-price" title="Price: ' + active + '"><strong class="text-dark">' + active + '</strong><span class="text-muted opacity-50">' + inactive + '</span></span>';
49    }
50
51    var badgeBg = m.badge_bg || themeColor;
52    var badgeIconHtml = m.badge_icon ? '<i class="' + m.badge_icon + ' me-1"></i>' : '';
53    var badgeHtml = m.badge ? '<span class="ed-card-badge ed-popup-badge" style="background-color:' + badgeBg + ' !important; color:#ffffff !important;">' + badgeIconHtml + m.badge + '</span>' : '';
54
55    return '<div class="ed-map-infowindow">' +
56      (m.image ? '<div class="ed-popup-media">' +
57        '<a href="' + m.url + '" class="ed-card-img-link" tabindex="-1">' +
58        '<img src="' + m.image + '" alt="' + (m.title || '') + '" loading="lazy" onerror="this.style.opacity=\'0\';">' +
59        '</a>' +
60        badgeHtml +
61        '</div>' : '') +
62      '<div class="ed-popup-body">' +
63      '<div class="ed-popup-main-info">' +
64      '<div class="ed-popup-title-row">' +
65      '<h4 class="ed-popup-title m-0"><a href="' + m.url + '">' + m.title + '</a></h4>' +
66      '</div>' +
67      (m.category ? '<div class="ed-popup-subtitle text-muted">' + m.category + '</div>' : '') +
68      '<div class="ed-card-submeta text-muted small">' +
69      (m.area ? '<span class="ed-submeta-area"><i class="fa-solid fa-location-dot me-1 text-primary"></i>' + m.area + '</span>' : '') +
70      (m.area && priceHtml ? '<span class="ed-meta-dot">•</span>' : '') +
71      priceHtml +
72      '</div>' +
73      '</div>' +
74      '<div class="ed-popup-action mt-2 pt-2 border-top">' +
75      '<a href="' + m.url + '" class="ed-btn ed-btn-primary ed-popup-btn w-100">' +
76      '<span>View Details</span> <i class="fa-solid fa-arrow-right ms-1"></i>' +
77      '</a>' +
78      '</div>' +
79      '</div>' +
80      '</div>';
81  }
82
83  EasyDirectory.initSplitMap = function(elementId, retries) {
84    retries = retries || 0;
85    var el = document.getElementById(elementId);
86    if (!el) return;
87
88    var provider = (el.dataset.provider || 'google').toLowerCase();
89    var rawData = el.dataset.markers;
90    var markers = [];
91    try {
92      markers = rawData ? JSON.parse(rawData) : [];
93    } catch (e) {
94      markers = [];
95    }
96
97    if (provider === 'google') {
98      if (typeof google !== 'undefined' && typeof google.maps !== 'undefined' && typeof google.maps.Map === 'function') {
99        initGoogleMap(el, markers);
100      } else if (retries < 25) {
101        // Wait for asynchronous Google Maps script to finish downloading
102        setTimeout(function() {
103          EasyDirectory.initSplitMap(elementId, retries + 1);
104        }, 120);
105      } else if (typeof L !== 'undefined' && typeof L.map === 'function') {
106        // Fallback to Leaflet if Google Maps failed to load
107        initLeafletMap(el, markers);
108      }
109    } else {
110      if (typeof L !== 'undefined' && typeof L.map === 'function') {
111        initLeafletMap(el, markers);
112      } else if (retries < 25) {
113        setTimeout(function() {
114          EasyDirectory.initSplitMap(elementId, retries + 1);
115        }, 100);
116      }
117    }
118  };
119
120  EasyDirectory.initMiniMap = function(elementId, lat, lng, title, retries) {
121    retries = retries || 0;
122    var el = document.getElementById(elementId);
123    if (!el || isNaN(lat) || isNaN(lng)) return;
124
125    var provider = (el.dataset.provider || 'google').toLowerCase();
126    var themeColor = getThemeColor();
127
128    if (provider === 'google') {
129      if (typeof google !== 'undefined' && typeof google.maps !== 'undefined' && typeof google.maps.Map === 'function') {
130        var center = { lat: lat, lng: lng };
131        var map = new google.maps.Map(el, {
132          zoom: 15,
133          center: center,
134          disableDefaultUI: true,
135          zoomControl: true,
136          styles: [
137            { featureType: 'poi', elementType: 'labels', stylers: [{ visibility: 'off' }] }
138          ]
139        });
140
141        var svgData = 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(getMarkerSvg(themeColor, false));
142        new google.maps.Marker({
143          position: center,
144          map: map,
145          title: title || '',
146          icon: {
147            url: svgData,
148            scaledSize: new google.maps.Size(36, 46),
149            anchor: new google.maps.Point(18, 46)
150          }
151        });
152      } else if (retries < 25) {
153        setTimeout(function() {
154          EasyDirectory.initMiniMap(elementId, lat, lng, title, retries + 1);
155        }, 120);
156      } else if (typeof L !== 'undefined' && typeof L.map === 'function') {
157        // Fallback to Leaflet
158        var map = L.map(elementId, {
159          zoomControl: false,
160          attributionControl: false
161        }
161).setView([lat, lng], 15);
162
163        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
164          maxZoom: 19,
165          attribution: ''
166        }).addTo(map);
167
168        L.control.zoom({ position: 'topleft' }).addTo(map);
169
170        var iconHtml = getMarkerSvg(themeColor, false);
171        var customIcon = L.divIcon({
172          className: 'ed-leaflet-svg-pin',
173          html: iconHtml,
174          iconSize: [36, 46],
175          iconAnchor: [18, 46],
176          popupAnchor: [0, -42]
177        });
178
179        L.marker([lat, lng], { icon: customIcon })
180          .addTo(map)
181          .bindPopup('<div style="font-family:inherit; font-weight:700; font-size:14px; padding:2px;">' + (title || '') + '</div>');
182      }
183    } else {
184      if (typeof L !== 'undefined' && typeof L.map === 'function') {
185        var map = L.map(elementId, {
186          zoomControl: false,
187          attributionControl: false
188        }).setView([lat, lng], 15);
189
190        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
191          maxZoom: 19,
192          attribution: ''
193        }).addTo(map);
194
195        L.control.zoom({ position: 'topleft' }).addTo(map);
196
197        var iconHtml = getMarkerSvg(themeColor, false);
198        var customIcon = L.divIcon({
199          className: 'ed-leaflet-svg-pin',
200          html: iconHtml,
201          iconSize: [36, 46],
202          iconAnchor: [18, 46],
203          popupAnchor: [0, -42]
204        });
205
206        L.marker([lat, lng], { icon: customIcon })
207          .addTo(map)
208          .bindPopup('<div style="font-family:inherit; font-weight:700; font-size:14px; padding:2px;">' + (title || '') + '</div>');
209      } else if (retries < 25) {
210        setTimeout(function() {
211          EasyDirectory.initMiniMap(elementId, lat, lng, title, retries + 1);
212        }, 100);
213      }
214    }
215  };
216
217  function initGoogleMap(el, markers) {
218    if (el.dataset.mapInitialized) return;
219    el.dataset.mapInitialized = 'true';
220
221    var defaultCenter = { lat: 43.7592, lng: 15.7794 };
222    var bounds = new google.maps.LatLngBounds();
223
224    var map = new google.maps.Map(el, {
225      zoom: 13,
226      center: defaultCenter,
227      mapTypeControl: false,
228      streetViewControl: false,
229      fullscreenControl: false,
230      zoomControl: true,
231      styles: [
232        { featureType: 'poi.business', stylers: [{ visibility: 'off' }] }
233      ]
234    });
235
236    activeMapInstance = map;
237    activeMapType = 'google';
238    activeInfoWindow = new google.maps.InfoWindow({
239      maxWidth: 250,
240      minWidth: 240,
241      disableAutoPan: false
242    });
243    activeMarkersMap = {};
244    activeMarkerDataMap = {};
245    activeMarkerList = [];
246    activeBounds = bounds;
247
248    EasyDirectory.addMapMarkers(markers);
249    observeMapSize(el);
250  }
251
252  function initLeafletMap(el, markers) {
253    if (el.dataset.mapInitialized) return;
254    el.dataset.mapInitialized = 'true';
255
256    var defaultCenter = [43.7592, 15.7794];
257    var map = L.map(el, {
258      zoomControl: true,
259      attributionControl: false
260    }).setView(defaultCenter, 13);
261
262    L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
263      maxZoom: 19,
264      attribution: ''
265    }).addTo(map);
266
267    activeMapInstance = map;
268    activeMapType = 'leaflet';
269    activeMarkersMap = {};
270    activeMarkerDataMap = {};
271    activeMarkerList = [];
272
273    EasyDirectory.addMapMarkers(markers);
274    observeMapSize(el);
275
276    setTimeout(function() {
277      map.invalidateSize();
278    }, 250);
279  }
280
281  function observeMapSize(el) {
282    if (!el || typeof ResizeObserver === 'undefined') {
283      return;
284    }
285    if (el.dataset.edResizeObserved === 'true') {
286      return;
287    }
288    el.dataset.edResizeObserved = 'true';
289
290    var raf = 0;
291    var observer = new ResizeObserver(function() {
292      if (raf) {
293        cancelAnimationFrame(raf);
294      }
295      raf = requestAnimationFrame(function() {
296        raf = 0;
297        if (activeMapType === 'leaflet' && activeMapInstance) {
298          activeMapInstance.invalidateSize();
299        } else if (activeMapType === 'google' && activeMapInstance && typeof google !== 'undefined') {
300          google.maps.event.trigger(activeMapInstance, 'resize');
301        }
302      });
303    });
304
305    observer.observe(el);
306    if (el.parentElement && el.parentElement.classList.contains('ed-split-map-col')) {
307      observer.observe(el.parentElement);
308    }
309  }
310
311  EasyDirectory.addMapMarkers = function(markers) {
312    if (!markers || !markers.length || !activeMapInstance) return;
313
314    var themeColor = getThemeColor();
315
316    if (activeMapType === 'google') {
317      var map = activeMapInstance;
318      var infoWindow = activeInfoWindow;
319      var bounds = activeBounds || new google.maps.LatLngBounds();
320
321      markers.forEach(function(m) {
322        if (!m.lat || !m.lng) return;
323        var pos = { lat: parseFloat(m.lat), lng: parseFloat(m.lng) };
324        bounds.extend(pos);
325
326        var isSpecial = !!m.badge || (m.badge === 'TOP PICK');
327        var svgData = 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(getMarkerSvg(themeColor, isSpecial));
328
329        var marker = new google.maps.Marker({
330          position: pos,
331          map: map,
332          title: m.title,
333          icon: {
334            url: svgData,
335            scaledSize: new google.maps.Size(36, 46),
336            anchor: new google.maps.Point(18, 46)
337          }
338        });
339
340        activeMarkersMap[m.id] = marker;
341        activeMarkersMap[String(m.id)] = marker;
342        activeMarkerDataMap[m.id] = m;
343        activeMarkerDataMap[String(m.id)] = m;
344        activeMarkerList.push(marker);
345
346        marker.addListener('click', function() {
347          var html = buildPopupHtml(m, themeColor);
348          infoWindow.setContent(html);
349          infoWindow.open({
350            map: map,
351            anchor: marker,
352            shouldFocus: false
353          });
354          highlightListingCard(m.id);
355          ensureInfoWindowInView(map);
356        });
357      });
358
359      if (activeMarkerList.length > 1) {
360        map.fitBounds(bounds, { top: 40, right: 40, bottom: 40, left: 40 });
361      } else if (activeMarkerList.length === 1) {
362        map.setCenter({ lat: parseFloat(markers[0].lat), lng: parseFloat(markers[0].lng) });
363        map.setZoom(15);
364      }
365    } else if (activeMapType === 'leaflet') {
366      var map = activeMapInstance;
367      var group = [];
368
369      markers.forEach(function(m) {
370        if (!m.lat || !m.lng) return;
371        var latLng = [parseFloat(m.lat), parseFloat(m.lng)];
372        group.push(latLng);
373
374        var isSpecial = !!m.badge || (m.badge === 'TOP PICK');
375        var iconHtml = getMarkerSvg(themeColor, isSpecial);
376
377        var customIcon = L.divIcon({
378          className: 'ed-leaflet-svg-pin',
379          html: iconHtml,
380          iconSize: [36, 46],
381          iconAnchor: [18, 46],
382          popupAnchor: [0, -42]
383        });
384
385        var popupHtml = buildPopupHtml(m, themeColor);
386        var marker = L.marker(latLng, { icon: customIcon }).addTo(map).bindPopup(popupHtml, {
387          maxWidth: 240,
388          minWidth: 240,
389          className: 'ed-leaflet-popup',
390          autoPanPadding: [15, 15]
391        });
392        activeMarkersMap[m.id] = marker;
393        activeMarkersMap[String(m.id)] = marker;
394        activeMarkerDataMap[m.id] = m;
395        activeMarkerDataMap[String(m.id)] = m;
396        activeMarkerList.push(marker);
397
398        marker.on('click', function() {
399          highlightListingCard(m.id);
400        });
401      });
402
403      if (group.length > 1) {
404        map.fitBounds(group, { padding: [40, 40] });
405      } else if (group.length === 1 && activeMarkerList.length === 1) {
406        map.setView(group[0], 15);
407      }
408    }
409  };
410
411  EasyDirectory.setMapMarkers = function(markers) {
412    if (!activeMapInstance) return;
413
414    // Clear existing markers
415    if (activeMapType === 'google') {
416      activeMarkerList.forEach(function(m) {
417        m.setMap(null);
418      });
419      activeMarkerList = [];
420      activeMarkersMap = {};
421      activeMarkerDataMap = {};
422      activeBounds = new google.maps.LatLngBounds();
423    } else if (activeMapType === 'leaflet') {
424      activeMarkerList.forEach(function(m) {
425        activeMapInstance.removeLayer(m);
426      });
427      activeMarkerList = [];
428      activeMarkersMap = {};
429      activeMarkerDataMap = {};
430    }
431
432    EasyDirectory.addMapMarkers(markers);
433  };
434
435  EasyDirectory.setUserLocationAndRadius = function(lat, lng, radius, unit) {
436    if (!activeMapInstance) return;
437
438    var themeColor = getThemeColor();
439
440    // Clean up existing circle and user marker
441    if (activeMapType === 'leaflet') {
442      if (activeRadiusCircle) {
443        activeMapInstance.removeLayer(activeRadiusCircle);
444        activeRadiusCircle = null;
445      }
446      if (activeUserMarker) {
447        activeMapInstance.removeLayer(activeUserMarker);
448        activeUserMarker = null;
449      }
450
451      if (lat && lng && radius > 0) {
452        var numLat = parseFloat(lat);
453        var numLng = parseFloat(lng);
454        var numRadius = parseFloat(radius);
455        var meters = (unit === 'mi') ? (numRadius * 1609.34) : (numRadius * 1000);
456
457        // Draw translucent radius circle
458        activeRadiusCircle = L.circle([numLat, numLng], {
459          radius: meters,
460          color: themeColor,
461          fillColor: themeColor,
462          fillOpacity: 0.12,
463          weight: 2,
464          dashArray: '5, 6'
465        }).addTo(activeMapInstance);
466
467        // User pulse dot marker
468        var userIcon = L.divIcon({
469          className: 'ed-user-location-icon',
470          html: '<div class="ed-user-pulse-dot" style="background:' + themeColor + ';"></div>',
471          iconSize: [20, 20],
472          iconAnchor: [10, 10]
473        });
474        activeUserMarker = L.marker([numLat, numLng], { icon: userIcon, zIndexOffset: 2000 }).addTo(activeMapInstance);
475
476        activeMapInstance.fitBounds(activeRadiusCircle.getBounds(), { padding: [30, 30] });
477      }
478    } else if (activeMapType === 'google') {
479      if (activeRadiusCircle) {
480        activeRadiusCircle.setMap(null);
481        activeRadiusCircle = null;
482      }
483      if (activeUserMarker) {
484        activeUserMarker.setMap(null);
485        activeUserMarker = null;
486      }
487
488      if (lat && lng && radius > 0) {
489        var numLat = parseFloat(lat);
490        var numLng = parseFloat(lng);
491        var numRadius = parseFloat(radius);
492        var meters = (unit === 'mi') ? (numRadius * 1609.34) : (numRadius * 1000);
493
494        var centerLatLng = new google.maps.LatLng(numLat, numLng);
495
496        activeRadiusCircle = new google.maps.Circle({
497          center: centerLatLng,
498          radius: meters,
499          strokeColor: themeColor,
500          strokeOpacity: 0.8,
501          strokeWeight: 2,
502          fillColor: themeColor,
503          fillOpacity: 0.12,
504          map: activeMapInstance
505        });
506
507        activeUserMarker = new google.maps.Marker({
508          position: centerLatLng,
509          map: activeMapInstance,
510          icon: {
511            path: google.maps.SymbolPath.CIRCLE,
512            scale: 7,
513            fillColor: themeColor,
514            fillOpacity: 1,
515            strokeColor: '#ffffff',
516            strokeWeight: 2.5
517          },
518          zIndex: 99999
519        });
520
521        activeMapInstance.fitBounds(activeRadiusCircle.getBounds());
522      }
523    }
524  };
525
526  EasyDirectory.resizeMap = function() {
527    function doResize() {
528      if (activeMapType === 'leaflet' && activeMapInstance) {
529        activeMapInstance.invalidateSize();
530      } else if (activeMapType === 'google' && activeMapInstance) {
531        google.maps.event.trigger(activeMapInstance, 'resize');
532      }
533    }
534    doResize();
535    setTimeout(doResize, 60);
536    setTimeout(doResize, 200);
537    setTimeout(doResize, 380);
538  };
539
540  EasyDirectory.highlightMarker = function(listingId, isHover) {
541    if (!listingId || !activeMapInstance) return;
542    var m = activeMarkersMap[listingId] || activeMarkersMap[String(listingId)];
543    if (!m) return;
544
545    if (activeMapType === 'leaflet') {
546      var el = (typeof m.getElement === 'function' ? m.getElement() : null) || m._icon;
547      if (el) {
548        var svg = el.querySelector('svg') || el;
549        if (isHover) {
550          svg.style.transition = 'transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1), filter 0.25s ease';
551          svg.style.transform = 'scale(1.4) translateY(-10px)';
552          svg.style.filter = 'drop-shadow(0 10px 20px rgba(37,99,235,0.7))';
553          if (typeof m.setZIndexOffset === 'function') {
554            m.setZIndexOffset(1000);
555          }
556        } else {
557          svg.style.transform = '';
558          svg.style.filter = '';
559          if (typeof m.setZIndexOffset === 'function') {
560            m.setZIndexOffset(0);
561          }
562        }
563      }
564    } else if (activeMapType === 'google') {
565      if (isHover) {
566        if (typeof m.setAnimation === 'function') {
567          m.setAnimation(google.maps.Animation.BOUNCE);
568        }
569        if (typeof m.setZIndex === 'function') {
570          m.setZIndex(99999);
571        }
572      } else {
573        if (typeof m.setAnimation === 'function') {
574          m.setAnimation(null);
575        }
576        if (typeof m.setZIndex === 'function') {
577          m.setZIndex(1);
578        }
579      }
580    }
581  };
582
583  function ensureInfoWindowInView(mapInstance) {
584    if (!mapInstance) return;
585    setTimeout(function() {
586      var iw = document.querySelector('.ed-component-wrapper .gm-style-iw-c') || document.querySelector('.gm-style-iw-c');
587      var mapDiv = typeof mapInstance.getDiv === 'function' ? mapInstance.getDiv() : document.getElementById('ed-split-map-canvas');
588      if (!iw || !mapDiv) return;
589
590      var iwRect = iw.getBoundingClientRect();
591      var mapRect = mapDiv.getBoundingClientRect();
592      var pad = 15;
593      var dx = 0;
594      var dy = 0;
595
596      if (iwRect.top < mapRect.top + pad) {
597        dy = iwRect.top - mapRect.top - pad;
598      } else if (iwRect.bottom > mapRect.bottom - pad) {
599        dy = iwRect.bottom - mapRect.bottom + pad;
600      }
601
602      if (iwRect.left < mapRect.left + pad) {
603        dx = iwRect.left - mapRect.left - pad;
604      } else if (iwRect.right > mapRect.right - pad) {
605        dx = iwRect.right - mapRect.right + pad;
606      }
607
608      if ((dx !== 0 || dy !== 0) && typeof mapInstance.panBy === 'function') {
609        mapInstance.panBy(dx, dy);
610      }
611    }, 50);
612  }
613
614  EasyDirectory.focusMarker = function(listingId) {
615    if (!listingId || !activeMapInstance) return;
616    var m = activeMarkersMap[listingId] || activeMarkersMap[String(listingId)];
617    if (!m) return;
618
619    if (activeMapType === 'leaflet') {
620      if (typeof m.getLatLng === 'function') {
621        var curZoom = typeof activeMapInstance.getZoom === 'function' ? activeMapInstance.getZoom() : 13;
622        var targetZoom = curZoom < 12 ? 14 : curZoom;
623        activeMapInstance.setView(m.getLatLng(), targetZoom, { animate: true, duration: 0.5 });
624      }
625      if (typeof m.openPopup === 'function') {
626        m.openPopup();
627      }
628    } else if (activeMapType === 'google') {
629      var themeColor = getThemeColor();
630      var data = activeMarkerDataMap[listingId] || activeMarkerDataMap[String(listingId)];
631      if (data && activeInfoWindow) {
632        if (typeof activeMapInstance.getZoom === 'function' && activeMapInstance.getZoom() < 12) {
633          activeMapInstance.setZoom(14);
634        }
635        activeInfoWindow.setContent(buildPopupHtml(data, themeColor));
636        activeInfoWindow.open({
637          anchor: m,
638          map: activeMapInstance,
639          shouldFocus: false
640        });
641        ensureInfoWindowInView(activeMapInstance);
642      }
643    }
644  };
645
646  function highlightListingCard(listingId) {
647    var card = document.querySelector('[data-listing-id="' + listingId + '"]');
648    if (!card) return;
649
650    var container = document.querySelector('.ed-split-content');
651    if (container) {
652      var topOffset = card.offsetTop - container.offsetTop - 16;
653      container.scrollTo({ top: topOffset, behavior: 'smooth' });
654    }
655
656    document.querySelectorAll('.ed-card-horizontal, .ed-card').forEach(function(c) {
657      c.classList.remove('ed-card-highlighted');
658    });
659
660    card.classList.add('ed-card-highlighted');
661    setTimeout(function() {
662      card.classList.remove('ed-card-highlighted');
663    }, 2000);
664  }
665
666  // Global Delegated Listeners for Two-Way Card -> Map Interaction
667  document.addEventListener('mouseover', function(e) {
668    var card = e.target.closest('[data-listing-id]');
669    if (!card) return;
670    var id = card.getAttribute('data-listing-id');
671    if (id) {
672      EasyDirectory.highlightMarker(id, true);
673    }
674  });
675
676  document.addEventListener('mouseout', function(e) {
677    var card = e.target.closest('[data-listing-id]');
678    if (!card) return;
679    var id = card.getAttribute('data-listing-id');
680    if (id) {
681      EasyDirectory.highlightMarker(id, false);
682    }
683  });
684
685  document.addEventListener('click', function(e) {
686    var card = e.target.closest('[data-listing-id]');
687    if (!card) return;
688    // Don't intercept clicks on links, buttons, or favorite icons
689    if (e.target.closest('a, button, input, select, textarea, label')) return;
690    var id = card.getAttribute('data-listing-id');
691    if (id) {
692      EasyDirectory.focusMarker(id);
693    }
694  });
695})();

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.