PageSourceSearch

https://spi-workshop.org/wp-content/plugins/easy-map/public/js/easy-map-store-locator.js?ver=2.0.0

js spi-workshop.org collected 2026-10-04 13:29:37 UTC 21,415 bytes, 499 lines download raw bytes

1/* global L, easy_map_set */
2(function () {
3    'use strict';
4
5    function initStoreLocator(cfg) {
6        var mapId     = cfg.mapId;
7        var zoomLevel = cfg.zoomLevel;
8        var parentId  = cfg.parentId;
9        var storeData = cfg.storeData;
10
11        var easy_map = L.map(mapId, {
12            zoomControl: false,
13            scrollWheelZoom: cfg.scrollWheel
14        }).setView([cfg.lat, cfg.lon], zoomLevel);
15
16        if (cfg.zoomControl) {
17            L.control.zoom({ position: cfg.zoomPos }).addTo(easy_map);
18        }
19
20        if (cfg.fullScreen) {
21            easy_map.addControl(L.control.fullscreen());
22        }
23
24        if (cfg.tilelayer === 'bing') {
25            L.tileLayer.bing({
26                bingMapsKey: cfg.bingKey,
27                imagerySet: 'Road'
28            }).addTo(easy_map);
29        } else if (cfg.tilelayer === 'google') {
30            L.gridLayer.googleMutant({ id: 'roads', type: 'roadmap' }).addTo(easy_map);
31        } else if (cfg.tilelayer === 'carto') {
32            var cartoUrl = 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png';
33            if (cfg.cartoKey) {
34                cartoUrl += '?key=' + encodeURIComponent(cfg.cartoKey);
35            }
36            L.tileLayer(
37                cartoUrl,
38                { attribution: '&copy; <a href="https://github.com/gravitystorm/openstreetmap-carto">OpenStreetMap Carto</a> contributors' }
39            ).addTo(easy_map);
40        } else {
41            L.tileLayer(
42                'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
43                { attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }
44            ).addTo(easy_map);
45        }
46
47        // Geo Search Control
48        if (cfg.geoSearch && typeof GeoSearch !== 'undefined') {
49            var search = new GeoSearch.GeoSearchControl({
50                notFoundMessage: 'Sorry, that address could not be found.',
51                provider: new GeoSearch.OpenStreetMapProvider(),
52                style: cfg.geoSearchStyle || 'bar'
53            });
54            easy_map.addControl(search);
55
56            easy_map.on('geosearch/showlocation', function (result) {
57                var loc = result.location;
58                L.marker([loc.y, loc.x])
59                    .addTo(easy_map)
60                    .bindPopup(loc.label)
61                    .openPopup();
62            });
63        }
64
65        function hasValue(text) {
66            return text !== null && text !== undefined && text !== '';
67        }
68
69        function escapeHtml(text) {
70            if (!hasValue(text)) { return ''; }
71            var div = document.createElement('div');
72            div.textContent = String(text);
73            return div.innerHTML;
74        }
75
76        var DAY_NAMES = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
77
78        function dayIndexFromToken(token) {
79            token = token.trim().toLowerCase();
80            for (var d = 0; d < DAY_NAMES.length; d++) {
81                if (token === DAY_NAMES[d] || token.indexOf(DAY_NAMES[d]) === 0) { return d; }
82            }
83            return -1;
84        }
85
86        // Parses free-text like "Sunday", "Sat, Sun", "Sat-Sun" into a Set of
87        // closed weekday indices (0=Sun..6=Sat), or null if nothing recognizable found.
88        function parseClosingDays(text) {
89            if (!hasValue(text)) { return null; }
90            var closed = new Set();
91            var parts = String(text).split(/,|&|\band\b/i);
92            for (var i = 0; i < parts.length; i++) {
93                var part = parts[i].trim();
94                if (!part) { continue; }
95
96                var rangeMatch = part.match(/^([a-z]+)\s*-\s*([a-z]+)$/i);
97                if (rangeMatch) {
98                    var startIdx = dayIndexFromToken(rangeMatch[1]);
99                    var endIdx   = dayIndexFromToken(rangeMatch[2]);
100                    if (startIdx !== -1 && endIdx !== -1) {
101                        var d = startIdx;
102                        while (true) {
103                            closed.add(d);
104                            if (d === endIdx) { break; }
105                            d = (d + 1) % 7;
106                        }
107                        continue;
108                    }
109                }
110
111                var idx = dayIndexFromToken(part);
112                if (idx !== -1) { closed.add(idx); }
113            }
114            return closed.size ? closed : null;
115        }
116
117        function to24Hour(hour, minute, meridiem) {
118            hour = hour % 12;
119            if (meridiem === 'pm') { hour += 12; }
120            return hour * 60 + minute;
121        }
122
123        // Parses free-text like "9:00 AM - 6:00 PM", "9am-5pm", "09:00-18:00"
124        // into {start, end} minutes-since-midnight, or null if unrecognized.
125        function parseHoursRange(text) {
126            if (!hasValue(text)) { return null; }
127            var re = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)?\s*(?:-|–|to)\s*(\d{1,2})(?::(\d{2}))?\s*(am|pm)?/i;
128            var m = String(text).match(re);
129            if (!m) { return null; }
130
131            var startHour = parseInt(m[1], 10);
132            var startMin  = m[2] ? parseInt(m[2], 10) : 0;
133            var startMer  = m[3] ? m[3].toLowerCase() : null;
134            var endHour   = parseInt(m[4], 10);
135            var endMin    = m[5] ? parseInt(m[5], 10) : 0;
136            var endMer    = m[6] ? m[6].toLowerCase() : null;
137
138            if (startHour > 23 || endHour > 23 || startMin > 59 || endMin > 59) { return null; }
139
140            var start, end;
141            if (startHour > 12 || endHour > 12) {
142                // An hour above 12 can only be 24-hour format (e.g. "09:00-18:00").
143                start = startHour * 60 + startMin;
144                end   = endHour * 60 + endMin;
145            } else if (!startMer && !endMer) {
146                // No am/pm anywhere — assume conventional business hours (start=am, end=pm).
147                start = to24Hour(startHour, startMin, 'am');
148                end   = to24Hour(endHour, endMin, 'pm');
149            } else {
150                // Infer a missing side's meridiem from the one that was given.
151                start = to24Hour(startHour, startMin, startMer || endMer);
152                end   = to24Hour(endHour, endMin, endMer || startMer);
153            }
154
155            return { start: start, end: end };
156        }
157
158        // Reads the current wall-clock day/hour/minute in the site's configured
159        // timezone (cfg.tzOffsetMinutes), independent of the visitor's own clock.
160        function getStoreLocalNow() {
161            var offsetMs = (cfg.tzOffsetMinutes || 0) * 60000;
162            var shifted  = new Date(Date.now() + offsetMs);
163            return {
164                day: shifted.getUTCDay(),
165                minutes: shifted.getUTCHours() * 60 + shifted.getUTCMinutes()
166            };
167        }
168
169        function isWithinHoursRange(range, minutesNow) {
170            if (range.end >= range.start) {
171                return minutesNow >= range.start && minutesNow < range.end;
172            }
173            // Overnight range (e.g. 10pm-2am) wraps past midnight.
174            return minutesNow >= range.start || minutesNow < range.end;
175        }
176
177        function computeStoreStatus(store, localNow) {
178            var closedDays = parseClosingDays(store.closing_day);
179            var hoursRange = parseHoursRange(store.opening_hours);
180
181            if (closedDays && closedDays.has(localNow.day)) {
182                return { open: false };
183            }
184            if (hoursRange) {
185                return { open: isWithinHoursRange(hoursRange, localNow.minutes) };
186            }
187            return null;
188        }
189
190        function statusBadgeHtml(status) {
191            if (!status) { return ''; }
192            var cls   = status.open ? 'easy-map-status-open' : 'easy-map-status-closed';
193            var label = status.open ? 'Open Now' : 'Closed';
194            return '<span class="easy-map-status-badge ' + cls + '">' + label + '</span>';
195        }
196
197        function markerIconFor(store) {
198            if (!store.marker_icon) { return undefined; }
199            var iconSize, iconAnchor, popupAnchor, shadowSize;
200            if (store.default_marker) {
201                iconSize = [25, 41];
202                iconAnchor = [12, 41];
203                popupAnchor = [1, -34];
204                shadowSize = [41, 41];
205            } else {
206                iconSize = [32, 37];
207                iconAnchor = [16, 37];
208                popupAnchor = [1, -37];
209                shadowSize = [37, 37];
210            }
211            return L.icon({
212                iconUrl: store.marker_icon,
213                shadowUrl: cfg.markerShadowUrl,
214                iconSize: iconSize,
215                iconAnchor: iconAnchor,
216                popupAnchor: popupAnchor,
217                shadowSize: shadowSize
218            });
219        }
220
221        var categoryLayers = {};
222        var allStores      = [];
223
224        // Build marker layers and sidebar lists
225        for (var idx in storeData) {
226            if (!storeData.hasOwnProperty(idx)) { continue; }
227            var grp       = storeData[idx];
228            var locations = grp.locations;
229            var layerKey  = parentId + '_' + grp.id;
230
231            categoryLayers[layerKey] = L.featureGroup();
232            var ul = document.querySelector('.easy-map-locations[data-cat="' + layerKey + '"]');
233
234            for (var j = 0; j < locations.length; j++) {
235                (function (store, layer) {
236                    allStores.push(store);
237
238                    var pinIcon = markerIconFor(store);
239                    var pin = L.marker([store.lat, store.lon], pinIcon ? { icon: pinIcon } : {});
240
241                    store._marker  = pin;
242					store._catKey  = layerKey;
243
244                    pin.addTo(layer);
245                    if (store.popup) {
246                        var popupContent = statusBadgeHtml(computeStoreStatus(store, getStoreLocalNow())) + store.popup;
247                        if (cfg.showDirections) {
248                            var directionsUrl = 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(store.lat + ',' + store.lon);
249                            popupContent += '<div style="margin-top:8px;"><a href="' + directionsUrl + '" target="_blank" rel="noopener noreferrer" class="easy-map-directions-btn"><span class="dashicons dashicons-location-alt"></span> Get Directions</a></div>';
250                        }
251                        pin.bindPopup(popupContent);
252                    }
253
254                    var li = document.createElement('li');
255                    li.textContent = store.title;
256                    li.addEventListener('click', function () {
257                        easy_map.setView([store.lat, store.lon], zoomLevel);
258                        if (store.popup) { pin.openPopup(); } else { easy_map.closePopup(); }
259                    });
260                    if (ul) { ul.appendChild(li); }
261                })(locations[j], categoryLayers[layerKey]);
262            }
263        }
264
265        // Category checkbox toggles
266        var toggles = document.querySelectorAll('.easy-map-cat-toggle');
267        for (var t = 0; t < toggles.length; t++) {
268            toggles[t].addEventListener('change', function () {
269                var cat   = this.getAttribute('data-cat');
270                var catUl = document.querySelector('.easy-map-locations[data-cat="' + cat + '"]');
271                var layer = categoryLayers[cat];
272                if (!layer) { return; }
273                if (this.checked) {
274                    layer.addTo(easy_map);
275                    if (catUl) { catUl.style.display = 'block'; }
276                } else {
277                    easy_map.removeLayer(layer);
278                    if (catUl) { catUl.style.display = 'none'; }
279                }
280            });
281        }
282
283        // Default: enable all categories
284        for (var didx in storeData) {
285            if (!storeData.hasOwnProperty(didx)) { continue; }
286            var dgrp = storeData[didx];
287            var cb   = document.querySelector('.easy-map-cat-toggle[data-cat="' + parentId + '_' + dgrp.id + '"]');
288            if (cb) {
289                cb.checked = true;
290                cb.dispatchEvent(new Event('change'));
291            }
292        }
293
294        if (typeof easy_map_set === 'function') { easy_map_set(easy_map); }
295
296        var filteredStores     = allStores.slice();
297        var currentPage        = 1;
298        var paginationEnabled  = !!cfg.paginationEnabled;
299        var perPage             = cfg.perPage > 0 ? cfg.perPage : 12;
300        var pageItems           = [];
301
302        var listEl       = document.getElementById('easy-map-store-list');
303        var paginationEl = document.getElementById('easy-map-pagination');
304
305        function scrollToMap() {
306            var mapEl = document.getElementById(mapId);
307            if (!mapEl) { return; }
308            var top = mapEl.getBoundingClientRect().top + window.pageYOffset;
309            try {
310                window.scrollTo({ top: top, behavior: 'smooth' });
311            } catch (e) {
312                window.scrollTo(0, top);
313            }
314        }
315
316        // Store card click — delegated, no per-render re-binding
317        listEl.addEventListener('click', function (e) {
318            var btn = e.target;
319            while (btn !== listEl) {
320                if (!btn) { break; }
321                if (btn.classList && btn.classList.contains('easy-map-store-btn')) { break; }
322                btn = btn.parentNode;
323            }
324            if (!btn || !btn.classList || !btn.classList.contains('easy-map-store-btn')) { return; }
325
326            var i     = parseInt(btn.getAttribute('data-index'), 10);
327            var store = pageItems[i];
328            if (!store) { return; }
329
330            easy_map.setView([store.lat, store.lon], zoomLevel);
331            if (store.popup) {
332                var clickPopupContent = statusBadgeHtml(computeStoreStatus(store, getStoreLocalNow())) + store.popup;
333                var clickIcon = markerIconFor(store);
334                L.marker([store.lat, store.lon], clickIcon ? { icon: clickIcon } : {}).addTo(easy_map).bindPopup(clickPopupContent).openPopup();
335            } else {
336                easy_map.closePopup();
337            }
338            scrollToMap();
339        });
340
341        function renderStores() {
342            if (paginationEnabled) {
343                var start = (currentPage - 1) * perPage;
344                pageItems = filteredStores.slice(start, start + perPage);
345            } else {
346                pageItems = filteredStores;
347            }
348
349            var html = [];
350            var localNow = getStoreLocalNow();
351            for (var i = 0; i < pageItems.length; i++) {
352                var s = pageItems[i];
353                var statusHtml = statusBadgeHtml(computeStoreStatus(s, localNow));
354
355                var addressParts = [s.address, s.city, s.state, s.country]
356                    .filter(hasValue)
357                    .map(escapeHtml);
358                var descHtml = addressParts.length
359                    ? '<div class="easy-map-store-desc">' +
360                        '<span class="dashicons dashicons-store"></span> ' +
361                        addressParts.join(', ') +
362                      '</div>'
363                    : '';
364
365                var infoRows = [];
366                if (hasValue(s.opening_hours)) {
367                    infoRows.push('<div><span class="dashicons dashicons-clock"></span> Open: ' + escapeHtml(s.opening_hours) + '</div>');
368                }
369                if (hasValue(s.closing_day)) {
370                    infoRows.push('<div><span class="dashicons dashicons-lock"></span> Closed: ' + escapeHtml(s.closing_day) + '</div>');
371                }
372                if (hasValue(s.phone)) {
373                    infoRows.push('<div><span class="dashicons dashicons-phone"></span> ' + escapeHtml(s.phone) + '</div>');
374                }
375                if (hasValue(s.email)) {
376                    infoRows.push('<div><span class="dashicons dashicons-email"></span> ' + escapeHtml(s.email) + '</div>');
377                }
378                var infoHtml = infoRows.length
379                    ? '<div class="easy-map-store-info">' + infoRows.join('') + '</div>'
380                    : '';
381
382                var directionsBtn = '';
383                if (cfg.showDirections) {
384                    var directionsUrl = 'https://www.google.com/maps/dir/?api=1&destination=' + encodeURIComponent(s.lat + ',' + s.lon);
385                    directionsBtn = '<a href="' + directionsUrl + '" target="_blank" rel="noopener noreferrer" class="easy-map-directions-btn">' +
386                        '<span class="dashicons dashicons-location-alt"></span> Get Directions' +
387                    '</a>';
388                }
389
390                html.push(
391                    '<div class="easy-map-store-card">' +
392                        '<div class="easy-map-store-title">'  + escapeHtml(s.title) + '</div>' +
393                        statusHtml +
394                        descHtml +
395                        infoHtml +
396                        '<div class="easy-map-store-actions">' +
397                            '<button class="easy-map-store-btn" data-index="' + i + '">' +
398                                '<span class="dashicons dashicons-location"></span> View Location' +
399                            '</button>' +
400                            directionsBtn +
401                        '</div>' +
402                    '</div>'
403                );
404            }
405            listEl.innerHTML = html.join('');
406        }
407
408        // Pagination click — delegated
409        paginationEl.addEventListener('click', function (e) {
410            var btn = e.target;
411            if (btn.tagName !== 'BUTTON' || !btn.hasAttribute('data-page')) { return; }
412            currentPage = parseInt(btn.getAttribute('data-page'), 10);
413            renderStores();
414            renderPagination();
415        });
416
417        function renderPagination() {
418            if (!paginationEnabled) {
419                paginationEl.innerHTML = '';
420                return;
421            }
422            var totalPages = Math.ceil(filteredStores.length / perPage);
423            var html = [];
424            for (var i = 1; i <= totalPages; i++) {
425                html.push('<button data-page="' + i + '" class="' + (i === currentPage ? 'active' : '') + '">' + i + '</button>');
426            }
427            paginationEl.innerHTML = html.join('');
428        }
429
430        function applyFilters() {
431            var searchEl   = document.getElementById('easyMapSearchInput');
432            var categoryEl = document.getElementById('easyMapCategoryFilter');
433            if (!searchEl || !categoryEl) { return; }
434
435            var searchValue   = searchEl.value.toLowerCase();
436            var categoryValue = categoryEl.value;
437
438            filteredStores = allStores.filter(function (store) {
439                var title         = store.title ? store.title.toLowerCase() : '';
440                var matchTitle    = title.indexOf(searchValue) !== -1;
441                var matchCategory = categoryValue === '' || String(store.store_cat_id) === categoryValue;
442                return matchTitle && matchCategory;
443            });
444
445            currentPage = 1;
446            syncMapMarkers();
447            renderStores();
448            renderPagination();
449        }
450
451        // EVENT LISTENERS
452		document.getElementById("easyMapSearchInput").addEventListener("input", applyFilters);
453		document.getElementById("easyMapCategoryFilter").addEventListener("change", applyFilters);
454
455        // SYNC MAP MARKERS — show/hide markers to match filteredStores
456		function syncMapMarkers() {
457			const filteredIds = new Set(filteredStores.map(function(s) { return s.id; }));
458			allStores.forEach(function(store) {
459			    if (!store._marker || !store._catKey) return;
460				const group = categoryLayers[store._catKey];
461				if (!group) return;
462				if (filteredIds.has(store.id)) {
463					if (!group.hasLayer(store._marker)) {
464						group.addLayer(store._marker);
465					}
466				} else {
467					if (group.hasLayer(store._marker)) {
468						group.removeLayer(store._marker);
469					}
470				}
471			});
472		}
473
474        var searchInput = document.getElementById('easyMapSearchInput');
475        var catFilter   = document.getElementById('easyMapCategoryFilter');
476        if (searchInput) { searchInput.addEventListener('input', applyFilters); }
477        if (catFilter)   { catFilter.addEventListener('change', applyFilters); }
478
479        renderStores();
480        renderPagination();
481    }
482
483    function processConfigs() {
484        var configs = window.easyMapSLConfigs;
485        if (!configs) { return; }
486        for (var i = 0; i < configs.length; i++) {
487            if (!configs[i]._initialized) {
488                configs[i]._initialized = true;
489                initStoreLocator(configs[i]);
490            }
491        }
492    }
493
494    if (document.readyState === 'loading') {
495        document.addEventListener('DOMContentLoaded', processConfigs);
496    } else {
497        processConfigs();
498    }
499})();

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.