PageSourceSearch

https://russam.ru/local/templates/.default/js/city-select.js?17901725568865

js russam.ru collected 2026-10-02 03:37:35 UTC 8,865 bytes, 229 lines download raw bytes

1/**
2 * ГЛОБАЛЬНЫЙ компонент выбора города (GraphModal).
3 * Открытие: CitySelect.open({ current: '<code>' }) — current для подсветки.
4 * После выбора: регион пишется в сессию (delivery_region.php?action=set)
5 * и шлётся событие window 'cityselect:chosen' (detail: {code, name}).
6 * Подписчики сами реагируют (карточка — пересчёт блока «Как забрать?»).
7 */
8(function () {
9    'use strict';
10
11    var LAT_KBD = 'qwertyuiop[]asdfghjkl;\'zxcvbnm,.';
12    var CYR_KBD = 'йцукенгшщзхъфывапролджэячсмитьбю';
13    var layoutMap = (function () {
14        var m = {};
15        for (var i = 0; i < LAT_KBD.length; i++) {
16            m[LAT_KBD[i]] = CYR_KBD[i];
17            m[CYR_KBD[i]] = LAT_KBD[i];
18            m[LAT_KBD[i].toUpperCase()] = CYR_KBD[i].toUpperCase();
19            m[CYR_KBD[i].toUpperCase()] = LAT_KBD[i].toUpperCase();
20        }
21        return m;
22    })();
23
24    function swapLayout(s) {
25        var out = '';
26        for (var i = 0; i < s.length; i++) out += layoutMap[s[i]] || s[i];
27        return out;
28    }
29
30    var POPULAR = ['Москва', 'Санкт-Петербург', 'Новосибирск', 'Екатеринбург', 'Казань', 'Красноярск'];
31
32    var cities = null; // справочник, в памяти до перезагрузки
33    var fuse = null;
34    var currentCode = '';
35
36    window.CitySelect = {
37        open: function (opts) {
38            opts = opts || {};
39            currentCode = opts.current || '';
40
41            var input = searchInput();
42            if (input) input.value = '';
43
44            if (window.ModalManager && typeof window.ModalManager.open === 'function') {
45                window.ModalManager.open('city-select');
46            }
47            ensureData();
48        }
49    };
50
51    /* ---------- DOM ---------- */
52
53    function modalBox() { return document.querySelector('[data-graph-target="city-select"]'); }
54    function listBox() { var m = modalBox(); return m ? m.querySelector('.city-select__list') : null; }
55    function searchInput() { var m = modalBox(); return m ? m.querySelector('.city-select__search input') : null; }
56
57    /* ---------- ДАННЫЕ (1 запрос на страницу) ---------- */
58
59    function ensureData() {
60        if (cities && cities.length) { renderList(cities, false); return; }
61
62        var box = listBox();
63        if (box) box.innerHTML = '<div class="city-select__empty">Загрузка…</div>';
64
65        fetch('/local/ajax/delivery_cities.php', { credentials: 'same-origin' })
66            .then(function (r) { return r.json(); })
67            .then(function (res) {
68                var data = (res && res.success && Array.isArray(res.data)) ? res.data : [];
69                if (!data.length) {
70                    if (box) box.innerHTML = '<div class="city-select__empty">Не удалось загрузить список городов</div>';
71                    return;
72                }
73                cities = data;
74                initFuse();
75                renderList(cities, false);
76            })
77            .catch(function () {
78                if (box) box.innerHTML = '<div class="city-select__empty">Не удалось загрузить список городов</div>';
79            });
80    }
81
82    function initFuse() {
83        fuse = null;
84        if (typeof Fuse !== 'undefined' && cities && cities.length) {
85            fuse = new Fuse(cities, {
86                keys: ['name'],
87                threshold: 0.35,
88                ignoreLocation: true,
89                minMatchCharLength: 2
90            });
91        }
92    }
93
94    /* ---------- РЕНДЕР ---------- */
95
96    function renderList(list, isSearch) {
97        var box = listBox();
98        if (!box) return;
99        if (!list || !list.length) {
100            box.innerHTML = '<div class="city-select__empty">Ничего не найдено</div>';
101            return;
102        }
103
104        var html = '';
105
106        // популярные — только в полном списке
107        if (!isSearch) {
108            var popular = [];
109            POPULAR.forEach(function (name) {
110                for (var i = 0; i < list.length; i++) {
111                    if (list[i].name === name) { popular.push(list[i]); break; }
112                }
113            });
114            if (popular.length) {
115                html += '<div class="city-select__popular">';
116                popular.forEach(function (c) { html += cityBtn(c, true); });
117                html += '</div>';
118            }
119        }
120
121        var sorted = list.slice().sort(function (a, b) { return a.name.localeCompare(b.name, 'ru'); });
122
123        // группы по букве (в поиске буквы не ставим)
124        var groups = [];
125        var last = null;
126        sorted.forEach(function (c) {
127            var letter = isSearch ? '' : c.name.charAt(0).toUpperCase();
128            if (letter !== last) { groups.push({ letter: letter, items: [] }); last = letter; }
129            groups[groups.length - 1].items.push(c);
130        });
131
132        html += '<div class="city-select__cols">';
133        groups.forEach(function (g) {
134            html += '<div class="city-select__group">';
135            if (g.letter) html += '<span class="city-select__letter">
135' + escapeHtml(g.letter) + '</span>';
136            g.items.forEach(function (c) { html += cityBtn(c, false); });
137            html += '</div>';
138        });
139        html += '</div>';
140
141        box.innerHTML = html;
142    }
143
144    function cityBtn(c, popular) {
145        var active = (c.code === currentCode) ? ' city-select__item--active' : '';
146        var pop = popular ? ' city-select__item--popular' : '';
147        return '<button type="button" class="city-select__item' + active + pop +
148            '" data-city-code="' + escapeHtml(c.code) +
149            '" data-city-name="' + escapeHtml(c.name) + '">' + escapeHtml(c.name) + '</button>';
150    }
151
152    /* ---------- СОБЫТИЯ ---------- */
153
154    function bindEvents() {
155        var input = searchInput();
156        if (input) {
157            input.addEventListener('input', function () {
158                if (!cities) return;
159                var q = this.value.trim();
160                if (q === '') { renderList(cities, false); return; }
161
162                // ищем в обеих раскладках
163                var variants = [q];
164                var alt = swapLayout(q);
165                if (alt !== q) variants.push(alt);
166
167                var res = [];
168                var seen = {};
169                variants.forEach(function (v) {
170                    var part;
171                    if (fuse) {
172                        part = fuse.search(v).map(function (r) { return r.item; });
173                    } else {
174                        var vl = v.toLowerCase();
175                        part = cities.filter(function (c) { return c.name.toLowerCase().indexOf(vl) !== -1; });
176                    }
177                    part.forEach(function (c) {
178                        if (!seen[c.code]) { seen[c.code] = 1; res.push(c); }
179                    });
180                });
181                renderList(res, true);
182            });
183        }
184
185        var list = listBox();
186        if (list) {
187            list.addEventListener('click', function (e) {
188                var btn = e.target.closest('.city-select__item');
189                if (!btn) return;
190                choose(btn.getAttribute('data-city-code'), btn.getAttribute('data-city-name'));
191            });
192        }
193    }
194
195    /* ---------- ЕДИНЫЙ ВЫБОР ГОРОДА ---------- */
196
197    function choose(code, name) {
198        if (!code) return;
199
200        // закрываем только через GraphModal
201        if (window.ModalManager && typeof window.ModalManager.close === 'function') {
202            window.ModalManager.close();
203        }
204
205        // запоминаем регион, ПОТОМ уведомляем подписчиков
206        fetch('/local/ajax/delivery_region.php?action=set&code=' + encodeURIComponent(code) +
207            '&name=' + encodeURIComponent(name), { credentials: 'same-origin' })
208            .catch(function () {})
209            .then(function () {
210                try {
211                    window.dispatchEvent(new CustomEvent('cityselect:chosen', { detail: { code: code, name: name } }));
212                } catch (e) {}
213            });
214    }
215
216    /* ---------- СЛУЖЕБНЫЕ ---------- */
217
218    function escapeHtml(str) {
219        var div = document.createElement('div');
220        div.textContent = (str == null ? '' : String(str));
221        return div.innerHTML;
222    }
223
224    if (document.readyState === 'loading') {
225        document.addEventListener('DOMContentLoaded', bindEvents);
226    } else {
227        bindEvents();
228    }
229})();

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.