PageSourceSearch

https://jbstudio.pro/wp-content/themes/web-jb/js/map-init.js?ver=1.1.0

js jbstudio.pro collected 2026-09-30 21:45:52 UTC 17,363 bytes, 366 lines download raw bytes

1/**
2 * Карта в футере — Яндекс Карты, JS API 3.0.
3 *
4 * Инициализация ленивая: карта строится, когда футер подходит к вьюпорту
5 * (IntersectionObserver с запасом 600px). До этого ни SDK Яндекса, ни CSS
6 * контролов с jsdelivr не запрашиваются. Так и должно быть: раньше всё это
7 * грузилось с первым рендером футера, то есть на каждой странице, и тянуло
8 * хвост критического пути примерно на 6 секунд.
9 *
10 * Пользователь по-прежнему видит готовую карту — к моменту, когда футер
11 * попадает на экран, она уже отрисована. Побочный плюс: у посетителей,
12 * не доскроллевших до футера, сторонних запросов к Яндексу не возникает
13 * вовсе. Полноценным решением по кукам это не считать — при доскролле
14 * запрос уходит независимо от согласия, вопрос комплаенса открыт.
15 *
16 * Оформление подложки — JSON из «Редактора стилей карт» (js/map-style.json),
17 * маршрут — полилиния по заданным точкам, метки — свои.
18 *
19 * ВАЖНО (условия бесплатного использования API, п. 3): копирайты, логотип
20 * Яндекса и кнопка «Открыть в Картах» должны оставаться видимыми. Их нельзя
21 * перекрывать или отключать — это требование действует и в платной лицензии.
22 */
23(function () {
24	'use strict';
25
26	var cfg = window.jbMap || {};
27	var root = document.querySelector('[data-jb-map]');
28
29	if (!root || !cfg.key) {
30		return;
31	}
32
33	var started = false;
34
35	/**
36	 * Диагностика. Открывать URL загрузчика в отдельной вкладке БЕСПОЛЕЗНО:
37	 * у JS API 3.0 проверка Referer строгая, а в адресной строке заголовка нет —
38	 * ключ отобьётся «Invalid api key» даже когда он полностью исправен.
39	 * Единственный валидный тест — загрузка со страницы сайта.
40	 */
41	/**
42	 * Ключ для сравнения с кабинетом: начало и хвост. Целиком он и так лежит
43	 * в исходнике страницы, но в логе достаточно опознавательных знаков.
44	 */
45	function maskKey(k) {
46		if (!k) {
47			return '(пусто)';
48		}
49		return k.length > 14
50			? k.slice(0, 8) + '…' + k.slice(-4) + ' (' + k.length + ' симв.)'
51			: k + ' (' + k.length + ' симв.)';
52	}
53
54	/** Политика реферера документа — если она `no-referrer`, ключ отобьётся всегда. */
55	function referrerPolicy() {
56		try {
57			return new Request(location.href).referrerPolicy || '(по умолчанию браузера)';
58		} catch (e) {
59			return '(не удалось определить)';
60		}
61	}
62
63	function explainSdkError() {
64		if (!window.console) {
65			return;
66		}
67		console.warn(
68			'[jb-map] SDK Яндекс Карт не загрузился — 403 по ключу.\n' +
69			'Проверять НЕ через открытие URL в новой вкладке: там нет заголовка Referer,\n' +
70			'и ключ с ограничениями всегда вернёт «Invalid api key». Тест только со страницы.\n' +
71			'\n' +
72			'Браузер отправил Яндексу такой Referer:\n' +
73			'  origin:            ' + location.origin + '\n' +
74			'  домен для кабинета ' + location.hostname + '\n' +
75			'  политика реферера: ' + referrerPolicy() + '\n' +
76			'  ключ на странице:  ' + maskKey(cfg.key) + '\n' +
77			'Строка «домен для кабинета» должна СОВПАДАТЬ с записью в поле\n' +
78			'«Ограничение по HTTP Referer» символ в символ (или быть её поддоменом).\n' +
79			'\n' +
80			'Что проверить в кабинете (yandex.ru/maps-api/console):\n' +
81			'1. Ключ создан под продукт «JavaScript API и HTTP Геокодер»,\n' +
82			'   а не Static API / Геосаджест / MapKit. Сверь строку «ключ на странице»\n' +
83			'   с ключом в кабинете: поле ymaps_key могло остаться от прежнего ключа.\n' +
84			'2. Поле «Ограничение по HTTP Referer» ЗАПОЛНЕÐ
84Ðž: JS API 3.0 не работает\n' +
85			'   с ключами без него. Очищать список нельзя.\n' +
86			'3. Домен записан без схемы, порта и параметров.\n' +
87			'   Поддомены Яндекс добавляет сам.\n' +
88			'4. С момента последнего сохранения формы прошло 15 минут — отсчёт\n' +
89			'   перезапускается при каждой правке.\n' +
90			'5. Ключ активирован (в списке статус «Активен», а не «Ожидает»).'
91		);
92	}
93
94	/** Подключает SDK один раз и отдаёт промис готовности. */
95	function loadSdk() {
96		if (window.ymaps3) {
97			return window.ymaps3.ready;
98		}
99		var url = 'https://api-maps.yandex.ru/v3/?apikey=' + encodeURIComponent(cfg.key) + '&lang=ru_RU';
100		return new Promise(function (resolve, reject) {
101			var s = document.createElement('script');
102			// Referer обязателен: ключ JS API 3.0 живёт только с ограничением по
103			// домену. Политика самого документа могла бы его срезать (`no-referrer`
104			// из плагина безопасности или CDN) — на теге она переопределяется,
105			// поэтому origin уйдёт всегда.
106			s.referrerPolicy = 'origin';
107			s.src = url;
108			s.onload = function () {
109				if (!window.ymaps3) {
110					reject(new Error('SDK загрузился, но ymaps3 не появился'));
111					return;
112				}
113				window.ymaps3.ready.then(resolve, reject);
114			};
115			s.onerror = function () {
116				explainSdkError();
117				reject(new Error('SDK Яндекс Карт не загрузился — подробности выше.'));
118			};
119			document.head.appendChild(s);
120		});
121	}
122
123	/** Метка: подпись на плашке, оформление — токенами темы. */
124	function markerEl(text, modifier) {
125		var el = document.createElement('div');
126		el.className = 'jb-map__pin' + (modifier ? ' jb-map__pin--' + modifier : '');
127		el.textContent = text;
128		return el;
129	}
130
131	/**
132	 * Элементы управления зумом (+/-) — отдельный пакет JS API
133	 * (@yandex/ymaps3-default-ui-theme), в основной SDK не входит.
134	 * Подключается через CDN по инструкции из документации:
135	 * yandex.ru/maps-api/docs/js-api/reference-guide/packages.html.
136	 * Если пакет не загрузился (сеть, CDN недоступен) — карта работает и без
137	 * контролов, это не критичная ошибка.
138	 */
139	var controlsCssHref = 'https://cdn.jsdelivr.net/npm/@yandex/ymaps3-default-ui-theme/dist/esm/index.css';
140	function loadControlsPackage() {
141		if (!window.ymaps3 || !window.ymaps3.import) {
142			return Promise.resolve(null);
143		}
144		try {
145			window.ymaps3.import.registerCdn('https://cdn.jsdelivr.net/npm/{package}', [
146				'@yandex/[email protected]'
147			]);
148		} catch (e) {
149			// registerCdn для одного и того же пакета второй раз может бросить —
150			// не критично, пакет уже зарегистрирован.
151		}
152		if (!document.querySelector('link[data-jb-map-controls-css]')) {
153			var link = document.createElement('link');
154			link.rel = 'stylesheet';
155			link.href = controlsCssHref;
156			link.setAttribute('data-jb-map-controls-css', '');
157			document.head.appendChild(link);
158		}
159		return window.ymaps3.import('@yandex/ymaps3-default-ui-theme').catch(function (e) {
160			if (window.console) {
161				console.warn('[jb-map] Контрол зума не загрузился:', (e && e.message) ? e.message : e);
162			}
163			return null;
164		});
165	}
166
167	/**
168	 * Доступные имена внутри карты. Всё, что рисует SDK и пакет
169	 * @yandex/ymaps3-default-ui-theme, приходит без текста — только SVG,
170	 * ни подписи, ни aria-label. Отсюда провалы axe/Lighthouse
171	 * «Buttons must have discernible text» (кнопки зума) и
172	 * «Links must have discernible text» (логотип в копирайтах),
173	 * а скринридер и ИИ-агент читают их как безымянные элементы.
174	 * Опций для подписи ни SDK, ни пакет не дают, поэтому вешаем сами.
175	 *
176	 * Видимое ничего не меняется: п. 3 условий бесплатного использования API
177	 * требует, чтобы копирайты, логотип и «Открыть в Картах» оставались
178	 * видимыми, и aria-label этого не касается.
179	 *
180	 * Направление зума определяем по вертикальной позиции, а не по порядку
181	 * в DOM: верхняя кнопка — «+».
182	 *
183	 * @return bool true, когда подписаны и кнопки, и логотип.
184	 */
185	function labelMapA11y(scope) {
186		var buttons = scope.querySelectorAll('.ymaps3--zoom-control button');
187		var zoomDone = buttons.length > 1;
188
189		if (zoomDone) {
190			var labels = ['Приблизить карту', 'Отдалить карту'];
191			Array.prototype.slice.call(buttons).sort(function (a, b) {
192				return a.getBoundingClientRect().top - b.getBoundingClientRect().top;
193			}
193).forEach(function (btn, i) {
194				if (!btn.getAttribute('aria-label')) {
195					btn.setAttribute('aria-label', labels[i] || labels[labels.length - 1]);
196				}
197			});
198		}
199
200		var logo = scope.querySelector('.ymaps3--map-copyrights__logo');
201		if (logo && !logo.getAttribute('aria-label')) {
202			logo.setAttribute('aria-label', 'Яндекс Карты');
203		}
204
205		// Тайлы подложки — <img> без alt, отсюда «Image elements do not have
206		// [alt] attributes» в Lighthouse. Проставлять alt каждому бесполезно:
207		// SDK досоздаёт тайлы при любом зуме и перетаскивании, а наблюдатель
208		// к тому моменту уже отключён. Поэтому скрываем от AT весь слой
209		// тайлов — aria-hidden наследуется, и будущие тайлы тоже под ним.
210		// Смысл не теряется: отдельный квадрат подложки ничего не сообщает,
211		// а адрес и маршрут продублированы текстом в футере. Копирайты,
212		// логотип, кнопки зума и метки лежат вне слоя и остаются доступны.
213		var tiles = scope.querySelectorAll('.ymaps3--tile-layer__container');
214		Array.prototype.forEach.call(tiles, function (layer) {
215			if (!layer.getAttribute('aria-hidden')) {
216				layer.setAttribute('aria-hidden', 'true');
217			}
218			// Страховка для уже отрисованных тайлов, если проверка смотрит
219			// на сам <img> и не учитывает скрытого родителя.
220			Array.prototype.forEach.call(layer.querySelectorAll('img:not([alt])'), function (img) {
221				img.setAttribute('alt', '');
222			});
223		});
224
225		return zoomDone && !!logo && tiles.length > 0;
226	}
227
228	/**
229	 * Контролы и копирайты рендерятся асинхронно, уже после addChild, поэтому
230	 * ждём их появления наблюдателем. Наблюдатель отключается сразу после
231	 * подписи и страховочно по таймауту — если пакет так и не отрисовался.
232	 */
233	function watchMapA11y(scope) {
234		if (labelMapA11y(scope) || typeof MutationObserver !== 'function') {
235			return;
236		}
237		var observer = new MutationObserver(function () {
238			if (labelMapA11y(scope)) {
239				observer.disconnect();
240			}
241		});
242		observer.observe(scope, { childList: true, subtree: true });
243		setTimeout(function () {
244			observer.disconnect();
245		}, 10000);
246	}
247
248	function init() {
249		if (started) {
250			return;
251		}
252		started = true;
253		root.classList.add('is-loading');
254
255		loadSdk().then(function () {
256			return Promise.all([
257				fetch(cfg.style).then(function (r) { return r.json(); }),
258				loadControlsPackage()
259			]);
260		}).then(function (res) {
261			var customization = res[0];
262			var controlsPkg = res[1];
263
264			var YMap = ymaps3.YMap;
265			var YMapDefaultSchemeLayer = ymaps3.YMapDefaultSchemeLayer;
266			var YMapDefaultFeaturesLayer = ymaps3.YMapDefaultFeaturesLayer;
267			var YMapFeature = ymaps3.YMapFeature;
268			var YMapMarker = ymaps3.YMapMarker;
269			var YMapControls = ymaps3.YMapControls;
270
271			var holder = document.createElement('div');
272			holder.className = 'jb-map__canvas';
273			root.appendChild(holder);
274
275			// Предпочитаем границы: карта сама подберёт масштаб под маршрут,
276			// и вид не поедет, если точки позже поменяются. center/zoom —
277			// запасной вариант, если границы не заданы.
278			var location = cfg.bounds
279				? { bounds: cfg.bounds }
280				: { center: cfg.center, zoom: cfg.zoom };
281
282			var map = new YMap(holder, {
283				location: location,
284				// карта в футере — декоративная, страницу скроллить не мешаем
285				behaviors: ['drag', 'pinchZoom', 'dblClick']
286			});
287
288			map.addChild(new YMapDefaultSchemeLayer({
289				theme: 'dark',
290				customization: customization
291			}));
292			map.addChild(new YMapDefaultFeaturesLayer({}));
293
294			// Маршрут от метро до офиса — полилиÐ
294½Ð¸Ñ по точкам из настроек.
295			if (cfg.route && cfg.route.length > 1) {
296				map.addChild(new YMapFeature({
297					geometry: { type: 'LineString', coordinates: cfg.route },
298					style: {
299						stroke: [{ color: cfg.routeColor, width: 4 }]
300					}
301				}));
302			}
303
304			if (cfg.office) {
305				map.addChild(new YMapMarker(
306					{ coordinates: cfg.office, draggable: false },
307					markerEl(cfg.officeLabel, 'office')
308				));
309			}
310
311			if (cfg.metro) {
312				map.addChild(new YMapMarker(
313					{ coordinates: cfg.metro, draggable: false },
314					markerEl(cfg.metroLabel, 'metro')
315				));
316			}
317
318			// Контролы находятся слева: справа размещены фиксированные кнопки
319			// чата и «Наверх», которые иначе пересекаются с картой.
320			if (controlsPkg && controlsPkg.YMapZoomControl && YMapControls) {
321				map.addChild(
322					new YMapControls({ position: 'left' }).addChild(new controlsPkg.YMapZoomControl({}))
323				);
324			}
325
326			// Подписи нужны и без пакета контролов: логотип в копирайтах
327			// рисует сам SDK, и он тоже приходит без доступного имени.
328			watchMapA11y(holder);
329
330			root.classList.remove('is-loading');
331			root.classList.add('is-ready');
332		}).catch(function (e) {
333			root.classList.remove('is-loading');
334			root.classList.add('is-failed');
335			started = false;
336			if (window.console) {
337				console.warn('[jb-map]', (e && e.message) ? e.message : e);
338			}
339		});
340	}
341
342	/**
343	 * Старт по приближению футера. Запас 600px берём с четырёх сторон, чтобы
344	 * карта успела построиться до того, как попадёт в кадр. Наблюдатель
345	 * отключается после первого срабатывания: init() и сам защищён флагом
346	 * `started`, но держать живую подписку незачем.
347	 *
348	 * Без IntersectionObserver (старые браузеры) поведение прежнее —
349	 * инициализация сразу.
350	 */
351	if (typeof IntersectionObserver !== 'function') {
352		init();
353	} else {
354		var observer = new IntersectionObserver(function (entries) {
355			for (var i = 0; i < entries.length; i++) {
356				if (entries[i].isIntersecting) {
357					observer.disconnect();
358					init();
359					return;
360				}
361			}
362		}, { rootMargin: '600px' });
363
364		observer.observe(root);
365	}
366})();

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.