This commit is contained in:
egorzainullin
2026-08-08 11:53:42 +03:00
parent 42b6eff560
commit 155d7690ae
7 changed files with 95 additions and 1070 deletions
+34 -180
View File
@@ -471,38 +471,23 @@ function escapeHtml(value) {
.replace(/'/g, "'");
}
function normalizePriceItems(items) {
if (!Array.isArray(items)) {
return [];
}
return items.map((item) => ({
name: typeof item?.name === "string" ? item.name : "",
price: typeof item?.price === "string" ? item.price : "",
amount: typeof item?.amount === "string" ? item.amount : "",
unit: typeof item?.unit === "string" ? item.unit : "",
total: typeof item?.total === "string" ? item.total : "",
}));
}
function normalizePriceData(data) {
if (!Array.isArray(data)) {
return JSON.parse(JSON.stringify(fallbackPriceData));
}
return data.map((category) => {
const children = Array.isArray(category?.children)
? category.children.map((child) => ({
title: typeof child?.title === "string" ? child.title : "",
items: normalizePriceItems(child?.items),
return data.map((category) => ({
title: typeof category?.title === "string" ? category.title : "",
items: Array.isArray(category?.items)
? category.items.map((item) => ({
name: typeof item?.name === "string" ? item.name : "",
price: typeof item?.price === "string" ? item.price : "",
amount: typeof item?.amount === "string" ? item.amount : "",
unit: typeof item?.unit === "string" ? item.unit : "",
total: typeof item?.total === "string" ? item.total : "",
}))
: [];
return {
title: typeof category?.title === "string" ? category.title : "",
items: normalizePriceItems(category?.items),
children,
};
});
: [],
}));
}
async function loadPriceData() {
@@ -548,186 +533,55 @@ function buildPriceList(items) {
.join("");
}
function buildPriceTable(items) {
return `
<ul class="price-list">
<li class="price-list__head" aria-hidden="true">
<span>Виды работ</span>
<span>Цена</span>
<span>Кол-во</span>
<span>Ед.изм</span>
</li>
${buildPriceList(Array.isArray(items) ? items : [])}
</ul>
`;
}
function slugifyCategory(title) {
const map = {
а: "a",
б: "b",
в: "v",
г: "g",
д: "d",
е: "e",
ё: "e",
ж: "zh",
з: "z",
и: "i",
й: "y",
к: "k",
л: "l",
м: "m",
н: "n",
о: "o",
п: "p",
р: "r",
с: "s",
т: "t",
у: "u",
ф: "f",
х: "h",
ц: "ts",
ч: "ch",
ш: "sh",
щ: "sch",
ъ: "",
ы: "y",
ь: "",
э: "e",
ю: "yu",
я: "ya",
};
return String(title || "")
.toLowerCase()
.split("")
.map((ch) => map[ch] ?? ch)
.join("")
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
}
function openPriceCategoryFromHash() {
if (!priceAccordion) {
return;
}
const hash = decodeURIComponent(location.hash.replace(/^#/, "")).trim();
if (!hash) {
return;
}
const category = priceAccordion.querySelector(
`.price-category#${CSS.escape(hash)}`
);
if (!(category instanceof HTMLElement)) {
return;
}
const toggle = category.querySelector(".price-category__toggle");
const listWrap = toggle?.nextElementSibling;
if (
toggle instanceof HTMLElement &&
listWrap instanceof HTMLElement &&
listWrap.classList.contains("price-list-wrap")
) {
toggle.setAttribute("aria-expanded", "true");
listWrap.hidden = false;
}
requestAnimationFrame(() => {
const header = document.querySelector(".header");
const offset =
header instanceof HTMLElement ? header.getBoundingClientRect().height + 12 : 12;
const top = category.getBoundingClientRect().top + window.scrollY - offset;
window.scrollTo({ top: Math.max(0, top), behavior: "smooth" });
});
}
function renderPriceAccordion(priceData) {
if (!priceAccordion) {
return;
}
priceAccordion.innerHTML = priceData
.map((category) => {
const hasChildren =
Array.isArray(category.children) && category.children.length > 0;
const categoryId = slugifyCategory(category.title || "kategoriya");
if (hasChildren) {
const childrenHtml = category.children
.map(
(child) => `
<article class="price-subcategory">
<button class="price-subcategory__toggle" type="button" aria-expanded="false">
${escapeHtml(child.title || "Подкатегория")}
</button>
<div class="price-list-wrap" hidden>
${buildPriceTable(child.items)}
</div>
</article>
`
)
.join("");
return `
<article class="price-category" id="${escapeHtml(categoryId)}">
<button class="price-category__toggle" type="button" aria-expanded="false">
${escapeHtml(category.title || "Категория")}
</button>
<div class="price-list-wrap price-list-wrap--nested" hidden>
<div class="price-subcategories">
${childrenHtml}
</div>
</div>
</article>
`;
}
return `
<article class="price-category" id="${escapeHtml(categoryId)}">
.map(
(category) => `
<article class="price-category">
<button class="price-category__toggle" type="button" aria-expanded="false">
${escapeHtml(category.title || "Категория")}
</button>
<div class="price-list-wrap" hidden>
${buildPriceTable(category.items)}
<ul class="price-list">
<li class="price-list__head" aria-hidden="true">
<span>Виды работ</span>
<span>Цена</span>
<span>Кол-во</span>
<span>Ед.изм</span>
</li>
${buildPriceList(Array.isArray(category.items) ? category.items : [])}
</ul>
</div>
</article>
`;
})
`
)
.join("");
openPriceCategoryFromHash();
}
if (priceAccordion) {
loadPriceData().then(renderPriceAccordion);
window.addEventListener("hashchange", openPriceCategoryFromHash);
priceAccordion.addEventListener("click", (event) => {
const target = event.target;
if (!(target instanceof HTMLElement)) {
return;
}
const toggle = target.closest(
".price-category__toggle, .price-subcategory__toggle"
);
if (!toggle || !(toggle instanceof HTMLElement)) {
return;
}
if (target.classList.contains("price-category__toggle")) {
const category = target.closest(".price-category");
const listWrap = category ? category.querySelector(".price-list-wrap") : null;
if (!listWrap) {
return;
}
const listWrap = toggle.nextElementSibling;
if (!(listWrap instanceof HTMLElement) || !listWrap.classList.contains("price-list-wrap")) {
return;
const expanded = target.getAttribute("aria-expanded") === "true";
target.setAttribute("aria-expanded", String(!expanded));
listWrap.hidden = expanded;
}
const expanded = toggle.getAttribute("aria-expanded") === "true";
toggle.setAttribute("aria-expanded", String(!expanded));
listWrap.hidden = expanded;
});
}