1(function () { 2 'use strict'; 3 const root = document.querySelector('.vd-shell'); 4 if (!root) return; 5 const lang = root.dataset.lang; 6 const t = (ru, uk, en) => ({ru, uk, en})[lang] || ru; 7 const query = root.querySelector('#vd-query'); 8 const list = root.querySelector('#vd-search-results'); 9 const status = root.querySelector('#vd-search-status'); 10 let pending, timer; 11 const normalize = value => String(value).toLocaleLowerCase().normalize('NFKD').replace(/[â'`]/g, '').replace(/Ñ/g, 'е'); 12 function load() { 13 if (!pending) pending = fetch(root.dataset.endpoint, {credentials: 'same-origin'}).then(r => { 14 if (!r.ok) throw new Error('Directory unavailable'); 15 return r.json(); 16 }).then(rows => { 17 if (!Array.isArray(rows)) throw new Error('Invalid directory'); 18 return rows; 19 }).catch(err => { pending = null; throw err; }); 20 return pending; 21 } 22 async function search() { 23 const text = query.value.trim(); 24 list.replaceChildren(); list.hidden = true; 25 if (text.length < 2) {status.textContent = ''; return;} 26 status.textContent = t('ÐÑÑâ¦', 'ШÑкаÑâ¦', 'Searchingâ¦'); 27 try { 28 const rows = await load(); 29 if (query.value.trim() !== text) return; 30 const words = normalize(text).split(/\s+/); 31 const score = r => ((r.city_names || []).some(c => normalize(c) === normalize(text)) ? 500 : 0) + (normalize(r.title).includes(normalize(text)) ? 100 : 0) + (r.type !== 'ticket' ? 20 : 0); 32 const found = rows.filter(r => words.every(w => normalize([r.title, r.address, r.locality, r.search].join(' ')).includes(w))).sort((a, b) => score(b) - score(a)); 33 status.textContent = found.length ? t('Ðайдено: ', 'Ðнайдено: ', 'Found: ') + found.length + (found.length > 30 ? t('. ÐÐ¾ÐºÐ°Ð·Ð°Ð½Ñ Ð¿ÐµÑвÑе 30; ÑÑоÑниÑе название.', '. Ðоказано пеÑÑÑ 30; ÑÑоÑнÑÑÑ Ð½Ð°Ð·Ð²Ñ.', '. Showing the first 30; refine your search.') : '') : t('Совпадений неÑ. ÐопÑобÑйÑе название гоÑода или пÑежнее название ÑÑанÑии.', 'ÐбÑгÑв немаÑ. СпÑобÑйÑе Ð½Ð°Ð·Ð²Ñ Ð¼ÑÑÑа або колиÑÐ½Ñ Ð½Ð°Ð·Ð²Ñ ÑÑанÑÑÑ.', 'No matches. Try the city or an earlier station name.'); 34 found.slice(0, 30).forEach(r => { 35 const li = document.createElement('li'), a = document.createElement('a'), address = document.createElement('small'); 36 a.href = r.url; a.textContent = r.title; address.textContent = r.address; 37 li.append(a, address); list.append(li); 38 }); 39 list.hidden = !found.length; 40 } catch (_) {status.textContent = t('ÐÑÑÑÑÑй поиÑк недоÑÑÑпен. ÐажмиÑе «ÐайÑи» Ð´Ð»Ñ Ð¾Ð±ÑÑного поиÑка.', 'Швидкий поÑÑк недоÑÑÑпний. ÐаÑиÑнÑÑÑ Â«ÐнайÑи» Ð´Ð»Ñ Ð·Ð²Ð¸Ñайного поÑÑкÑ.', 'Quick search is unavailable. Press Search to use the standard search.');} 41 } 42 query.addEventListener('input', () => {clearTimeout(timer); timer = setTimeout(search, 200);}); 43 const mapElement = document.getElementById('vd-map'); 44 const dataElement = document.getElementById('vd-map-data'); 45 function initializeMap() { 46 if (!mapElement || !dataElement || !window.L || mapElement.dataset.ready) return; 47 mapElement.dataset.ready = '1'; 48 const rows = JSON.parse(dataElement.textContent); 49 if (!rows.length) mapElement.textContent = t('ÐÐµÑ Ð¿Ð¾Ð´ÑвеÑждÑннÑÑ ÐºÐ¾Ð¾ÑÐ´Ð¸Ð½Ð°Ñ Ð´Ð»Ñ Ð¾Ð±Ñей каÑÑÑ.', 'ÐÐµÐ¼Ð°Ñ Ð¿ÑдÑвеÑÐ´Ð¶ÐµÐ½Ð¸Ñ ÐºÐ¾Ð¾ÑÐ´Ð¸Ð½Ð°Ñ Ð´Ð»Ñ ÑпÑлÑÐ½Ð¾Ñ ÐºÐ°ÑÑи.', 'There are no confirmed coordinates for this map.'); 50 else { 51 const map = L.map(mapElement, {scrollWheelZoom: false}); 52 map.attributionControl.setPrefix(map.attributionControl.options.prefix.replace('<a ', '<a rel="nofollow noopener" ')); 53 L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { 54 maxZoom: 19, attribution: '© <a rel="nofollow" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' 55 }).addTo(map); 56 const colors = {rail: '#176b4c', bus: '#235ac0', airport: '#8b4bbc', marine: '#007f98'}; 57 rows.forEach(r => { 58 const content = document.createElement('div'), link = document.createElement('a'), address = document.createElement('p'); 59 link.href = r.url; link.textContent = r.title; address.textContent = r.address; 60 content.append(link, address); 61 L.circleMarker(r.point, {radius: 8, color: '#fff', weight: 2, fillColor: colors[r.type] || '#6b7280', fillOpacity: 1}).addTo(map).bindPopup(content); 62 }); 63 map.fitBounds(rows.map(r => r.point), {padding: [28, 28], maxZoom: 14}); 64 } 65 } 66 if (mapElement && dataElement) { 67 let loading = false;
68 const startMap = () => { 69 if (loading || mapElement.dataset.ready) return; 70 loading = true; 71 if (window.L && !mapElement.dataset.loadFailed) {initializeMap(); return;} 72 const base = mapElement.dataset.leafletBase; 73 if (!base) return; 74 mapElement.textContent = t('ÐагÑÑжаем каÑÑÑâ¦', 'ÐаванÑажÑÑмо каÑÑÑâ¦', 'Loading mapâ¦'); 75 const css = document.createElement('link'); css.rel = 'stylesheet'; css.href = base + 'leaflet.css?ver=1.9.4'; 76 const script = document.createElement('script'); script.src = base + 'leaflet.js?ver=1.9.4'; script.async = true; 77 let styleReady = false, scriptReady = false; 78 const ready = () => {if(styleReady && scriptReady) {mapElement.replaceChildren(); initializeMap();}}; 79 css.onload = () => {styleReady = true; ready();}; 80 script.onload = () => {scriptReady = true; ready();}; 81 const failed = () => { 82 loading = false; mapElement.dataset.loadFailed = '1'; css.remove(); script.remove(); 83 const retry = document.createElement('button'); retry.type = 'button'; 84 retry.textContent = t('ÐовÑоÑиÑÑ Ð·Ð°Ð³ÑÑÐ·ÐºÑ ÐºÐ°ÑÑÑ', 'ÐовÑоÑиÑи заванÑÐ°Ð¶ÐµÐ½Ð½Ñ ÐºÐ°ÑÑи', 'Retry loading the map'); 85 retry.addEventListener('click', startMap); mapElement.replaceChildren(retry); 86 }; 87 script.onerror = failed; css.onerror = failed; 88 document.head.append(css); document.body.append(script); 89 }; 90 if ('IntersectionObserver' in window) { 91 const observer = new IntersectionObserver(entries => { 92 if (entries.some(entry => entry.isIntersecting)) {observer.disconnect(); startMap();} 93 }, {rootMargin: '300px'}); 94 observer.observe(mapElement); 95 } else {startMap();} 96 mapElement.addEventListener('focus', startMap, {once:true}); 97 } 98 const transfer = document.getElementById('vd-transfer'); 99 if (transfer) transfer.addEventListener('submit', event => { 100 event.preventDefault(); 101 const from = document.getElementById('vd-from').value, to = document.getElementById('vd-to').value; 102 if (from === to) { 103 document.getElementById('vd-transfer-note').textContent = t('ÐÑбеÑиÑе ÑазнÑе вокзалÑ.', 'ÐбеÑÑÑÑ ÑÑÐ·Ð½Ñ Ð²Ð¾ÐºÐ·Ð°Ð»Ð¸.', 'Choose different stations.'); return; 104 } 105 const url = new URL('https://www.google.com/maps/dir/'); 106 url.search = new URLSearchParams({api: '1', origin: from, destination: to, travelmode: 'transit'}).toString(); 107 window.location.assign(url.href); 108 }); 109})();
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.