1 2(() => { 3 const state = { 4 items: [], 5 filtered: [], 6 stats: null, 7 activeMapItem: null, 8 filters: { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] } 9 }; 10 11 document.addEventListener('DOMContentLoaded', () => { 12 document.querySelectorAll('[data-bmj-app]').forEach(initApp); 13 }); 14 15 async function initApp(app) { 16 if (!app || !window.BMJ_DATA) return; 17 app._bmj = { app }; 18 bindTabs(app); 19 bindFilterEvents(app); 20 bindResponsiveLayout(app); 21 22 try { 23 const [itemsRes, statsRes] = await Promise.all([ 24 fetch(BMJ_DATA.restUrl), 25 fetch(BMJ_DATA.statsUrl) 26 ]); 27 const allItems = await itemsRes.json(); 28 const stats = await statsRes.json(); 29 30 const singleId = Number(app.getAttribute('data-bmj-single-id') || 0); 31 const mode = app.getAttribute('data-bmj-mode') || 'full'; 32 33 app._bmj.items = singleId ? allItems.filter(i => Number(i.id) === singleId) : allItems; 34 app._bmj.stats = stats; 35 state.items = app._bmj.items; 36 state.stats = stats; 37 38 if (mode !== 'single') { 39 fillFilterOptions(app, state.items); 40 } 41 applyFilters(app); 42 if (mode !== 'single') renderStats(app, stats); 43 } catch (e) { 44 console.error('BMJ init error', e); 45 } 46 } 47 48 function bindResponsiveLayout(app) { 49 const apply = () => { 50 const width = app.clientWidth || app.offsetWidth || 0; 51 app.classList.toggle('bmj-shell--narrow', width > 0 && width < 680); 52 if (width >= 760) { 53 app.querySelectorAll('[data-bmj-panel]').forEach(panel => panel.classList.add('is-active')); 54 app.querySelectorAll('[data-bmj-tab]').forEach(button => button.classList.remove('is-active')); 55 } else if (!app.querySelector('[data-bmj-tab].is-active')) { 56 const firstTab = app.querySelector('[data-bmj-tab="map"]'); 57 const mapPanel = app.querySelector('[data-bmj-panel="map"]'); 58 const listPanel = app.querySelector('[data-bmj-panel="list"]'); 59 if (firstTab && mapPanel && listPanel) { 60 firstTab.classList.add('is-active'); 61 mapPanel.classList.add('is-active'); 62 listPanel.classList.remove('is-active'); 63 } 64 } 65 }; 66 if ('ResizeObserver' in window) { 67 const ro = new ResizeObserver(apply); 68 ro.observe(app); 69 } 70 window.addEventListener('resize', apply); 71 apply(); 72 } 73 74 function bindTabs(app) { 75 app.querySelectorAll('[data-bmj-tab]').forEach(button => { 76 button.addEventListener('click', () => { 77 const tab = button.getAttribute('data-bmj-tab'); 78 app.querySelectorAll('[data-bmj-tab]').forEach(b => b.classList.toggle('is-active', b === button)); 79 app.querySelectorAll('[data-bmj-panel]').forEach(panel => panel.classList.toggle('is-active', panel.getAttribute('data-bmj-panel') === tab)); 80 }); 81 }); 82 } 83 84 function bindFilterEvents(app) { 85 const filters = app.querySelector('[data-bmj-filters]'); 86 if (!filters) return; 87 filters.querySelectorAll('[data-filter]').forEach(el => { 88 el.addEventListener('input', (e) => onFilterChange(app, e)); 89 el.addEventListener('change', (e) => onFilterChange(app, e)); 90 }); 91 const reset = filters.querySelector('[data-bmj-reset]'); 92 if (reset) { 93 reset.addEventListener('click', () => { 94 filters.querySelectorAll('[data-filter]').forEach(el => el.value = ''); 95 app._bmj.filters = { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] }; 96 applyFilters(app); 97 }); 98 } 99 } 100 101 function onFilterChange(app, e) { 102 if (!app._bmj.filters) app._bmj.filters = { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] }; 103 app._bmj.filters[e.target.getAttribute('data-filter')] = e.target.value.trim(); 104 applyFilters(app); 105 } 106 107 function fillFilterOptions(app, items) { 108 fillSelect(app, 'country', uniq(items.map(i =>
108 i.country).filter(Boolean)).sort(localeSort)); 109 fillSelect(app, 'city', uniq(items.map(i => i.city).filter(Boolean)).sort(localeSort)); 110 fillSelect(app, 'tag', uniq(items.flatMap(i => Array.isArray(i.tags) ? i.tags : []).filter(Boolean)).sort(localeSort)); 111 fillQuickCountryFilters(app, items); 112 } 113 114 function fillQuickCountryFilters(app, items) { 115 const root = app.querySelector('[data-bmj-country-boxes]'); 116 if (!root) return; 117 root.querySelectorAll('label').forEach(el => el.remove()); 118 uniq(items.map(i => i.country).filter(Boolean)).sort(localeSort).forEach(value => { 119 const label = document.createElement('label'); 120 label.innerHTML = `<input type="checkbox" data-bmj-quick="country" value="${escapeAttr(value)}"> ${escapeHtml(value)}`; 121 label.querySelector('input').addEventListener('change', () => onQuickFilterChange(app)); 122 root.append(label); 123 }); 124 } 125 126 function fillSelect(app, name, values) { 127 const select = app.querySelector(`[data-filter="${name}"]`); 128 if (!select) return; 129 const first = select.querySelector('option'); 130 select.innerHTML = ''; 131 if (first) select.append(first); 132 values.forEach(value => { 133 const option = document.createElement('option'); 134 option.value = value; 135 option.textContent = value; 136 select.append(option); 137 }); 138 } 139 140 function applyFilters(app) { 141 const items = app._bmj.items || []; 142 const filters = app._bmj.filters || { search: '', country: '', city: '', visited: '', rating: '', tag: '', quickVisited: [], quickCountries: [], quickMinRatings: [] }; 143 144 app._bmj.filtered = items.filter(item => { 145 const haystack = [item.title, item.country, item.city, item.subtitle, item.address, ...(item.tags || [])].join(' ').toLowerCase(); 146 if (filters.search && !haystack.includes(filters.search.toLowerCase())) return false; 147 if (filters.country && item.country !== filters.country) return false; 148 if (filters.city && item.city !== filters.city) return false; 149 if (filters.visited !== '' && String(Number(item.visited)) !== filters.visited) return false; 150 if (filters.rating && Number(item.rating || 0) < Number(filters.rating)) return false; 151 if (filters.tag && !(item.tags || []).includes(filters.tag)) return false; 152 if (filters.quickVisited?.length && !filters.quickVisited.includes(String(Number(item.visited)))) return false; 153 if (filters.quickCountries?.length && !filters.quickCountries.includes(item.country)) return false; 154 if (filters.quickMinRatings?.length && !filters.quickMinRatings.some(min => Number(item.rating || 0) >= Number(min))) return false; 155 return true; 156 }); 157 158 renderList(app); 159 renderMap(app); 160 } 161
162 function renderStats(app, stats) { 163 const root = app.querySelector('[data-bmj-stats]'); 164 if (!root || !stats) return; 165 const cards = [ 166 ['ÐÑего', stats.total], 167 ['ÐоÑеÑено', stats.visited], 168 ['Ðе поÑеÑено', stats.notVisited], 169 ['СÑÑан', stats.countries], 170 ['ÐоÑодов', stats.cities] 171 ]; 172 root.innerHTML = cards.map(([label, value]) => ` 173 <div class="bmj-stat"> 174 <span class="bmj-stat__label">${escapeHtml(label)}</span> 175 <span class="bmj-stat__value">${escapeHtml(String(value))}</span> 176 </div>`).join(''); 177 } 178 179 function renderList(app) { 180 const root = app.querySelector('[data-bmj-list]'); 181 if (!root) return; 182 const items = app._bmj.filtered || []; 183 if (!items.length) { 184 root.innerHTML = `<div class="bmj-empty">${escapeHtml(BMJ_DATA.strings.empty)}</div>`; 185 return; 186 } 187 188 root.innerHTML = items.map(item => ` 189 <a class="bmj-card bmj-card--compact bmj-card--list-link" href="${escapeAttr(item.url)}"> 190 <div class="bmj-catalog-card-top"> 191 <span class="bmj-card__status ${item.visited ? 'is-visited' : 'is-not-visited'}">${item.visited ? escapeHtml(BMJ_DATA.strings.visited) : escapeHtml(BMJ_DATA.strings.notVisited)}</span> 192 <span class="bmj-catalog-mini-thumb ${item.image ? '' : 'bmj-catalog-mini-thumb--empty'}">${item.image ? `<img src="${escapeAttr(item.image)}" alt="" loading="lazy">` : ''}</span> 193 </div> 194 <h3>${escapeHtml(item.title)}</h3> 195 <p class="bmj-card__geo">${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}</p> 196 </a> 197 `).join(''); 198 199 root.querySelectorAll('[data-bmj-toggle]').forEach(button => { 200 button.addEventListener('click', () => toggleVisited(app, button.getAttribute('data-bmj-toggle'))); 201 }); 202 root.querySelectorAll('[data-bmj-focus]').forEach(button => { 203 button.addEventListener('click', () => focusOnItem(app, Number(button.getAttribute('data-bmj-focus')))); 204 }); 205 } 206 207 function renderMap(app) { 208 const root = app.querySelector('[data-bmj-map]'); 209 if (!root) return; 210 const items = app._bmj.filtered || []; 211 const mode = app.getAttribute('data-bmj-mode') || 'full'; 212 const plotted = items.filter(i => isFinite(Number(i.lat)) && isFinite(Number(i.lng)) && (Number(i.lat) || Number(i.lng))); 213 const provider = (BMJ_DATA.provider || '').toLowerCase(); 214 215 if (!root.dataset.bmjHeightLocked) { 216 const h = root.clientHeight || parseInt(app.getAttribute('data-height') || '560', 10) || 560; 217 root.style.minHeight = `${h}px`; 218 root.dataset.bmjHeightLocked = '1'; 219 } 220 221 if (provider === 'yandex' && BMJ_DATA.yandexKey) { 222 ensureYandexReady() 223 .then(() => renderYandexMap(root, plotted, app, mode)) 224 .catch(() => renderLeafletFallback(root, plotted, app, mode)); 225 return; 226 } 227 228 if (provider === 'google' && BMJ_DATA.googleKey) { 229 ensureGoogleReady() 230 .then(() => renderGoogleMap(root, plotted, app, mode)) 231 .catch(() => renderLeafletFallback(root, plotted, app, mode)); 232 return; 233 } 234 235 renderLeafletFallback(root, plotted, app, mode); 236 } 237 238 function ensureYandexReady() { 239 return new Promise((resolve, reject) => { 240 if (window.ymaps && typeof window.ymaps.ready === 'function') { 241 window.ymaps.ready(resolve); 242 return; 243 } 244 let tries = 0; 245 const timer = setInterval(() => { 246 tries += 1; 247 if (window.ymaps && typeof window.ymaps.ready === 'function') { 248 clearInterval(timer); 249 window.ymaps.ready(resolve); 250 return; 251 } 252 if (tries >= 20) { 253 clearInterval(timer); 254 reject(new Error('Yandex Maps not ready')); 255 } 256 }, 300); 257 }); 258 } 259 260 function ensureGoogleReady() { 261 return new Promise((resolve, reject) => { 262 if (window.google && window.google.maps) { 263 resolve(); 264 return; 265 } 266 let tries = 0; 267 const timer = setInterval(() => { 268 tries += 1; 269 if (window.google && window.google.maps) { 270 clearInterval(timer); 271 resolve(); 272 return; 273 } 274 if (tries >= 20) { 275 clearInterval(timer); 276 reject(new Error('Google Maps not ready')); 277 } 278 }, 300); 279 }); 280 } 281 282 function renderLeafletFallback(root, items, app, mode) { 283 if (window.L) { 284 renderLeafletMap(root, items, app, mode); 285 return; 286 } 287 const canvas = document.createElement('div'); 288 canvas.className = 'bmj-map__canvas'; 289 const note = document.createElement('div'); 290 note.className = 'bmj-map__fallback-note'; 291 note.textContent = BMJ_DATA.strings.mapFallback; 292 canvas.append(note); 293 294 if (!items.length) { 295 canvas.innerHTML += `<div class="bmj-empty" style="position:absolute;
295left:16px;right:16px;top:60px;">${escapeHtml(BMJ_DATA.strings.empty)}</div>`; 296 } else { 297 const clusters = clusterItems(items, Number(BMJ_DATA.clusterDistance || 0.45)); 298 clusters.forEach(cluster => { 299 const marker = document.createElement('button'); 300 marker.type = 'button'; 301 marker.className = 'bmj-marker ' + (cluster.visited ? 'is-visited' : 'is-not-visited') + (cluster.items.length > 1 ? ' is-cluster' : '') + ' ' + (cluster.items.length === 1 ? ('bmj-marker--' + (cluster.items[0].markerStyle || 'default')) : ''); 302 marker.style.left = `${cluster.x}%`; 303 marker.style.top = `${cluster.y}%`; 304 marker.innerHTML = `<span class="bmj-marker__dot">${cluster.items.length > 1 ? escapeHtml(String(cluster.items.length)) : (cluster.items[0].markerStyle==='custom' && cluster.items[0].markerIcon ? `<img src="${cluster.items[0].markerIcon}" style="width:20px;height:20px;">` : getMarkerSymbol(cluster.items[0]))}</span>`; 305 marker.addEventListener('click', () => { 306 if (cluster.items.length > 1) { 307 renderClusterCard(canvas, cluster, app); 308 } else { 309 renderMapCard(canvas, cluster.items[0], app); 310 } 311 }); 312 canvas.append(marker); 313 }); 314 } 315 316 root.innerHTML = ''; 317 root.append(canvas); 318 } 319 320 function renderYandexMap(root, items, app, mode) { 321 root.innerHTML = '<div class="bmj-yandex-map" style="width:100%;height:100%;min-height:inherit;"></div>'; 322 const mapEl = root.querySelector('.bmj-yandex-map'); 323 const defaults = BMJ_DATA.mapDefaults || { lat: 54.6872, lng: 25.2797, zoom: 4 }; 324 const first = items[0]; 325 const center = first ? [Number(first.lat), Number(first.lng)] : [Number(defaults.lat), Number(defaults.lng)]; 326 const zoom = mode === 'single' && first ? 13 : Number(defaults.zoom || 4); 327 328 try { 329 const map = new ymaps.Map(mapEl, { 330 center, 331 zoom, 332 controls: ['zoomControl'] 333 }); 334 335 const bounds = []; 336 items.forEach((item) => { 337 const lat = Number(item.lat); 338 const lng = Number(item.lng); 339 if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; 340 341 const builtInIcon = getBuiltInMarkerIcon(item); 342 const options = builtInIcon ? { 343 iconLayout: 'default#image', 344 iconImageHref: builtInIcon, 345 iconImageSize: [30, 30], 346 iconImageOffset: [-15, -30] 347 } : { 348 preset: item.visited ? 'islands#greenDotIcon' : 'islands#blueDotIcon', 349 iconColor: item.visited ? '#2e9d51' : '#2b78e4' 350 }; 351 const placemark = new ymaps.Placemark([lat, lng], { 352 balloonContentHeader: escapeHtml(item.title), 353 balloonContentBody: `${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}${item.rating ? `<br>РейÑинг: ${escapeHtml(String(item.rating))}` : ''}<br><a href="${escapeAttr(item.url)}">${escapeHtml(BMJ_DATA.strings.openPage)}</a>` 354 }, options); 355 356 map.geoObjects.add(placemark); 357 bounds.push([lat, lng]); 358 }); 359 360 if (bounds.length > 1 && mode !== 'single') { 361 map.setBounds(bounds, { checkZoomRange: true, zoomMargin: 30 }); 362 } else if (bounds.length === 1) { 363 map.setCenter(bounds[0], mode === 'single' ? 13 : 11); 364 try { 365 const firstGeoObject = map.geoObjects.get(0); 366 if (firstGeoObject) { 367 firstGeoObject.balloon.open(); 368 } 369 } catch (e) {} 370 } 371 372 setTimeout(() => { 373 try { map.container.fitToViewport(); } catch (e) {} 374 }, 120); 375 } catch (e) { 376 renderLeafletFallback(root, items, app, mode); 377 } 378 } 379 380 function renderGoogleMap(root, items, app, mode) { 381 root.innerHTML = '<div class="bmj-google-map" style="width:100%;height:100%;min-height:inherit;"></div>'; 382 const mapEl = root.querySelector('.bmj-google-map'); 383 const defaults = BMJ_DATA.mapDefaults || { lat: 54.6872, lng: 25.2797, zoom: 4 }; 384 const first = items[0]; 385 const center = first ? { lat: Number(first.lat), lng: Number(first.lng) } : { lat: Number(defaults.lat), lng: Number(defaults.lng) }; 386 const zoom = mode === 'single' && first ? 13 : Number(defaults.zoom || 4); 387 388 try { 389 const map = new google.maps.Map(mapEl, { center, zoom }); 390 const bounds = new google.maps.LatLngBounds(); 391 let hasBounds = false;
392 393 items.forEach((item) => { 394 const lat = Number(item.lat); 395 const lng = Number(item.lng); 396 if (!Number.isFinite(lat) || !Number.isFinite(lng)) return; 397 const builtInIcon = getBuiltInMarkerIcon(item); 398 const marker = new google.maps.Marker({ 399 position: { lat, lng }, 400 map, 401 title: item.title, 402 icon: builtInIcon ? { 403 url: builtInIcon, 404 scaledSize: new google.maps.Size(30, 30) 405 } : { 406 path: google.maps.SymbolPath.CIRCLE, 407 scale: 8, 408 fillColor: item.visited ? '#2e9d51' : '#2b78e4', 409 fillOpacity: 1, 410 strokeColor: '#ffffff', 411 strokeWeight: 2 412 } 413 }); 414 const info = new google.maps.InfoWindow({ 415 content: `<div><strong>${escapeHtml(item.title)}</strong><br>${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}${item.rating ? `<br>РейÑинг: ${escapeHtml(String(item.rating))}` : ''}<br><a href="${escapeAttr(item.url)}">${escapeHtml(BMJ_DATA.strings.openPage)}</a></div>` 416 }); 417 marker.addListener('click', () => info.open({ anchor: marker, map })); 418 if (mode === 'single') { 419 info.open({ anchor: marker, map }); 420 } 421 bounds.extend({ lat, lng }); 422 hasBounds = true; 423 }); 424 425 if (hasBounds && items.length > 1 && mode !== 'single') { 426 map.fitBounds(bounds); 427 } else if (hasBounds && items.length === 1) { 428 map.setCenter(bounds.getCenter()); 429 map.setZoom(mode === 'single' ? 13 : 11); 430 } 431 } catch (e) { 432 renderLeafletFallback(root, items, app, mode); 433 } 434 } 435 436 function renderClusterCard(canvas, cluster, app) { 437 removeExistingCard(canvas); 438 const card = document.createElement('div'); 439 card.className = 'bmj-map-card'; 440 card.innerHTML = ` 441 <div class="bmj-card__status ${cluster.visited ? 'is-visited' : 'is-not-visited'}">${escapeHtml(String(cluster.items.length))} ${escapeHtml(BMJ_DATA.strings.clusterLabel)}</div> 442 <div class="bmj-tag-list" style="margin-top:12px;"> 443 ${cluster.items.slice(0, 8).map(item => `<span>${escapeHtml(item.title)}</span>`).join('')} 444 </div> 445 `; 446 canvas.append(card); 447 } 448 449 function renderMapCard(canvas, item, app) { 450 removeExistingCard(canvas); 451 const card = document.createElement('div'); 452 card.className = 'bmj-map-card'; 453 card.innerHTML = ` 454 <div class="bmj-card__status ${item.visited ? 'is-visited' : 'is-not-visited'}"> 455 ${item.visited ? escapeHtml(BMJ_DATA.strings.visited) : escapeHtml(BMJ_DATA.strings.notVisited)} 456 </div> 457 <h3 style="margin:12px 0 8px;">${escapeHtml(item.title)}</h3> 458 <p class="bmj-card__geo">${escapeHtml([item.country, item.city].filter(Boolean).join(', '))}</p> 459 ${item.subtitle ? `<p>${escapeHtml(item.subtitle)}</p>` : ''} 460 <div class="bmj-card__meta"> 461 <span>РейÑинг: <strong>${escapeHtml(item.rating || 'â')}</strong></span> 462 <span>ÐаÑа: <strong>${escapeHtml(item.visitDate || 'â')}</strong></span> 463 </div> 464 ${item.tags?.length ? `<div class="bmj-tag-list">${item.tags.map(tag => `<span>${escapeHtml(tag)}</span>`).join('')}</div>` : ''} 465 <div class="bmj-card__meta"> 466 <a class="bmj-button" href="${escapeAttr(item.url)}">${escapeHtml(BMJ_DATA.strings.openPage)}</a> 467 </div> 468 `; 469 canvas.append(card); 470 } 471 472 async function toggleVisited(app, id) { 473 try { 474 const res = await fetch(BMJ_DATA.toggleUrl, { 475 method: 'POST', 476 headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': BMJ_DATA.nonce }, 477 body: JSON.stringify({ id: Number(id) }) 478 }); 479 if (!res.ok) return; 480 const payload = await res.json(); 481 const target = (app._bmj.items || []).find(i => Number(i.id) === Number(id)); 482 if (target) target.visited = !!payload.visited; 483 applyFilters(app); 484 } catch (e) { 485 console.error('BMJ toggle error', e); 486 } 487 } 488 489 function focusOnItem(app, id) { 490 app._bmj.focusItemId = id; 491 const mapPanel = app.querySelector('[data-bmj-panel="map"]'); 492 const listPanel = app.querySelector('[data-bmj-panel="list"]'); 493 const tab = app.querySelector('[data-bmj-tab="map"]'); 494 if (window.innerWidth < 760 && mapPanel && listPanel && tab) { 495 app.querySelectorAll('[data-bmj-tab]').forEach(b => b.classList.remove('is-active')); 496 tab.classList.add('is-active'); 497 mapPanel.classList.add('is-active'); 498 listPanel.classList.remove('is-active'); 499 } 500 renderMap(app); 501 app.querySelector('[data-bmj-map]')?.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 502 } 503 504 function clusterItems(items, threshold) { 505 const valid = items.map(item => ({ 506 ...item, 507 _x: normalize(item.lng, -180, 180, 3, 97), 508 _y: normalize(item.lat, 85, -85, 6, 94) 509 })); 510
511 const clusters = []; 512 valid.forEach(item => { 513 let cluster = clusters.find(c => distance(c.lat, c.lng, item.lat, item.lng) <= threshold); 514 if (!cluster) { 515 cluster = { items: [], lat: item.lat, lng: item.lng, x: item._x, y: item._y, visited: item.visited }; 516 clusters.push(cluster); 517 } 518 cluster.items.push(item); 519 cluster.visited = cluster.items.some(i => i.visited); 520 cluster.lat = avg(cluster.items.map(i => Number(i.lat))); 521 cluster.lng = avg(cluster.items.map(i => Number(i.lng))); 522 cluster.x = avg(cluster.items.map(i => i._x)); 523 cluster.y = avg(cluster.items.map(i => i._y)); 524 }); 525 return clusters; 526 } 527 528 function removeExistingCard(canvas) { 529 const card = canvas.querySelector('.bmj-map-card'); 530 if (card) card.remove(); 531 } 532 533 function normalize(value, min, max, outMin, outMax) { 534 const ratio = (Number(value) - min) / (max - min); 535 return outMin + Math.max(0, Math.min(1, ratio)) * (outMax - outMin); 536 } 537 function distance(lat1, lng1, lat2, lng2) { 538 const dx = Number(lat1) - Number(lat2); 539 const dy = Number(lng1) - Number(lng2); 540 return Math.sqrt(dx * dx + dy * dy); 541 } 542 function avg(arr) { return arr.reduce((a,b)=>a+b,0) / (arr.length || 1); } 543 function uniq(arr) { return [...new Set(arr)]; } 544 function localeSort(a, b) { return a.localeCompare(b, 'ru'); } 545 function escapeHtml(str) { 546 return String(str).replace(/[&<>"']/g, m => ({ '&':'&','<':'<','>':'>','"':'"', "'":''' }[m])); 547 } 548 549 550 function getBuiltInMarkerIcon(item) { 551 const style = (item && item.markerStyle) || 'default'; 552 const base = (window.BMJ_DATA && BMJ_DATA.iconBaseUrl) ? BMJ_DATA.iconBaseUrl : ''; 553 if (!base) return ''; 554 if (style === 'brewery') return `${base}marker-brewery.svg`; 555 if (style === 'brewpub') return `${base}marker-brewpub.svg`; 556 return ''; 557 } 558 559function getMarkerSymbol(item) { 560 switch ((item && item.markerStyle) || 'default') { 561 case 'brewery': return 'â'; 562 case 'brewpub': return 'ð»'; 563 case 'beer': return 'ðº'; 564 case 'star': return 'â '; 565 case 'pin': return '⬤'; 566 case 'dot': return 'â¢'; 567 default: return 'â'; 568 } 569 } 570 571function escapeAttr(str) { return escapeHtml(str); } 572})();
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.