PageSourceSearch

http://vokzal.org.ua/wp-content/plugins/vokzal-directory/directory.js?ver=0.2.10

js vokzal.org.ua collected 2026-09-25 09:08:00 UTC 7,388 bytes, 109 lines download raw bytes

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.