(() => {
"use strict";
const config = window.SALON;
const $ = (selector, parent = document) => parent.querySelector(selector);
const create = (tag, className, text) => {
const element = document.createElement(tag);
if (className) element.className = className;
if (text !== undefined) element.textContent = text;
return element;
};
const formatPrice = value => value.replace(/ /g, "\u00a0") + "\u00a0₽";
const safeUrl = value => {
try { const url = new URL(value); return url.protocol === "https:" ? url.href : null; }
catch { return null; }
};
const escapeHtml = text => String(text).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
const label = (key, fallback) => config.buttons[key]?.label || fallback;
const buttonUrl = value => { if (/^#[a-zA-Z0-9_-]*$/.test(value) || value === 'price.php') return value; try {const u=new URL(value);return ['https:','mailto:','tel:'].includes(u.protocol)?u.href:null;} catch{return null;} };
document.querySelectorAll('[data-control]').forEach(element => {
const item=config.buttons[element.dataset.control]; if(!item) return;
element.textContent=item.label; if(element.hasAttribute('aria-label')) element.setAttribute('aria-label',item.label.replace(/\n/g,' ')); if(element.classList.contains('round-booking')) element.style.whiteSpace='pre-line';
if(element.tagName==='A') element.href=buttonUrl(item.url)||'#';
if(element.hasAttribute('data-book') && item.mode==='url') {
element.removeAttribute('data-book');
element.addEventListener('click',()=>{const url=buttonUrl(item.url);if(url) location.assign(url);});
}
});
$('#booking-continue').textContent=label('continueBook','Перейти к записи');
$('#retry-map').textContent=label('retryMap','Загрузить карту');
let currentService = null;
let currentBranch = null;
let lastTrigger = null;
const dialog = $("#booking-dialog");
const tabs = $(".category-tabs");
const content = $("#price-content");
const setCategory = index => {
[...tabs.children].forEach((tab, i) => {
tab.setAttribute("aria-selected", String(i === index));
tab.tabIndex = i === index ? 0 : -1;
content.children[i].hidden = i !== index;
});
};
config.categories.forEach((category, index) => {
const tab = create("button", "category-tab");
tab.type = "button";
tab.id = `category-${index}`;
tab.setAttribute("role", "tab");
tab.setAttribute("aria-controls", `panel-${index}`);
tab.append(create("span", "", category.name), create("span", "tab-number", String(category.services.length).padStart(2, "0")));
tab.addEventListener("click", () => setCategory(index));
tab.addEventListener("keydown", event => {
let next;
if (["ArrowDown", "ArrowRight"].includes(event.key)) next = (index + 1) % config.categories.length;
if (["ArrowUp", "ArrowLeft"].includes(event.key)) next = (index - 1 + config.categories.length) % config.categories.length;
if (event.key === "Home") next = 0;
if (event.key === "End") next = config.categories.length - 1;
if (next !== undefined) { event.preventDefault(); setCategory(next); tabs.children[next].focus(); }
});
tabs.append(tab);
const panel = create("section", "price-panel");
panel.id = `panel-${index}`;
panel.setAttribute("role", "tabpanel");
panel.setAttribute("aria-labelledby", tab.id);
const heading = create("div", "panel-title");
heading.append(create("h3", "", category.name), create("p", "", category.note));
panel.append(heading);
category.services.forEach(([name, price]) => {
const row = create("div", "service-row");
const button = create("button", "service-book", label("serviceBook", "Записаться"));
button.type = "button";
button.setAttribute("aria-label", `${label("serviceBook", "Записаться")}: ${name}`);
button.title = `${label("serviceBook", "Записаться")}: ${name}`;
button.addEventListener("click", () => openBooking({name, price}, null, button));
row.append(create("span", "service-name", name), create("span", "service-price", formatPrice(price)), button);
panel.append(row);
});
content.append(panel);
});
setCategory(0);
const tabsMedia = matchMedia("(max-width:760px)");
const updateOrientation = () => tabs.setAttribute("aria-orientation", tabsMedia.matches ? "horizontal" : "vertical");
updateOrientation();
tabsMedia.addEventListener("change", updateOrientation);
function updateBooking() {
const branch = config.branches.find(item => item.key === currentBranch);
const serviceLink = branch && currentService && branch.serviceUrls[currentService.name];
const url = branch && (safeUrl(serviceLink) || safeUrl(branch.bookingUrl));
const next = $("#booking-continue");
next.setAttribute("aria-disabled", String(!url));
if (url) {
next.href = url;
next.removeAttribute("tabindex");
$("#booking-status").textContent = serviceLink
? "Выбранная услуга откроется в сервисе записи. Выберите там удобные дату и время."
: currentService
? "В сервисе записи выберите эту услугу, удобные дату и время."
: "Перейдите в сервис записи, чтобы выбрать услугу, дату и время.";
} else {
next.removeAttribute("href");
next.tabIndex = -1;
$("#booking-status").textContent = branch
? "Онлайн-запись в этот филиал пока недоступна. Пожалуйста, попробуйте позже."
: "Выберите удобный филиал, чтобы проверить доступность онлайн-записи.";
}
}
function openBooking(service, branchKey, trigger) {
lastTrigger = trigger;
currentService = service;
currentBranch = branchKey;
const selected = $("#selected-service");
selected.replaceChildren();
selected.hidden = !service;
if (service) selected.append(create("strong", "", "ВЫБРАННАЯ УСЛУГА"), create("span", "", `${service.name} · ${formatPrice(service.price)}`));
$("#booking-branches").querySelectorAll("input").forEach(input => { input.checked = input.value === branchKey; });
updateBooking();
dialog.showModal();
}
document.querySelectorAll("[data-book]").forEach(button => button.addEventListener("click", () => openBooking(null, null, button)));
$(".close-button").addEventListener("click", () => dialog.close());
dialog.addEventListener("click", event => {
const rect = dialog.getBoundingClientRect();
if (event.target === dialog && (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom)) dialog.close();
});
dialog.addEventListener("close", () => lastTrigger?.focus());
$("#booking-continue").addEventListener("click", event => {
if (event.currentTarget.getAttribute("aria-disabled") === "true") event.preventDefault();
});
config.branches.forEach((branch, index) => {
const card = create("article", "branch-card");
card.append(create("span", "branch-number", `ФИЛИАЛ / 0${index + 1}`), create("h3", "", branch.address));
if (branch.mapNote) card.append(create("p", "map-note", branch.mapNote));
const links = create("div", "branch-links");
const mapLink = create("a", "", label("mapLink", "Открыть в Яндекс Картах"));
mapLink.href = branch.mapsUrl;
const routeLink = create("a", "", label("routeLink", "Построить маршрут"));
const routeTarget = branch.coordinates ? branch.coordinates.join(",") : branch.fullAddress;
routeLink.href = branch.routeUrl || "https://yandex.ru/maps/?rtext=~" + encodeURIComponent(routeTarget) + "&rtt=auto";
[mapLink, routeLink].forEach(link => { link.target = "_blank"; link.rel = "noopener noreferrer"; });
links.append(mapLink, routeLink);
const book = create("button", "branch-book");
book.textContent = label("branchBook", "Записаться в этот филиал");
book.addEventListener("click", () => openBooking(null, branch.key, book));
card.append(links, book);
$("#branch-cards").append(card);
const choice = create("label", "branch-choice");
const radio = create("input");
radio.type = "radio"; radio.name = "branch"; radio.value = branch.key;
radio.addEventListener("change", () => { currentBranch = radio.value; updateBooking(); });
choice.append(radio, create("span", "", branch.address));
$("#booking-branches").append(choice);
});
let mapLoading = false;
let mapInstance;
function loadMap() {
if (mapLoading || mapInstance) return;
mapLoading = true;
const script = document.createElement("script");
script.src = "https://api-maps.yandex.ru/2.1/?lang=ru_RU" + (config.yandexApiKey ? "&apikey=" + encodeURIComponent(config.yandexApiKey) : "");
script.async = true;
let timer;
const fail = () => {
clearTimeout(timer);
mapLoading = false;
$("#map-shell").classList.remove("map-loaded");
$("#retry-map").hidden = false;
};
timer = setTimeout(fail, 15000);
script.onerror = fail;
script.onload = () => {
if (!window.ymaps) { fail(); return; }
window.ymaps.ready(() => {
try {
const points = config.branches.filter(branch => Array.isArray(branch.coordinates));
if (!points.length) { fail(); return; }
mapInstance = new window.ymaps.Map("map", {
center: points[0].coordinates, zoom: 14,
controls: ["zoomControl", "fullscreenControl"]
});
points.forEach((branch, index) => {
const placemark = new window.ymaps.Placemark(branch.coordinates, {
balloonContentHeader: escapeHtml(config.name),
balloonContentBody: escapeHtml(branch.address) + (branch.mapNote ? "
" + escapeHtml(branch.mapNote) : ""),
hintContent: `${config.name} — ${branch.address}`,
iconContent: String(index + 1)
}, {preset: "islands#circleIcon", iconColor: "#602631"});
mapInstance.geoObjects.add(placemark);
});
if (points.length > 1) mapInstance.setBounds(mapInstance.geoObjects.getBounds(), {checkZoomRange: true, zoomMargin: 65});
mapInstance.behaviors.disable("scrollZoom");
clearTimeout(timer);
$("#map-shell").classList.add("map-loaded");
} catch { mapInstance = null; fail(); }
});
};
document.head.append(script);
}
$("#retry-map").addEventListener("click", loadMap);
if ("IntersectionObserver" in window) {
const observer = new IntersectionObserver(entries => {
if (entries.some(entry => entry.isIntersecting)) { loadMap(); observer.disconnect(); }
}, {rootMargin: "350px"});
observer.observe($("#map-shell"));
} else loadMap();
})();