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: '© <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: '© <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.