обновлено
This commit is contained in:
@@ -471,23 +471,38 @@ 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) => ({
|
||||
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 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 {
|
||||
title: typeof category?.title === "string" ? category.title : "",
|
||||
items: normalizePriceItems(category?.items),
|
||||
children,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function loadPriceData() {
|
||||
@@ -533,55 +548,186 @@ 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) => `
|
||||
<article class="price-category">
|
||||
.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)}">
|
||||
<button class="price-category__toggle" type="button" aria-expanded="false">
|
||||
${escapeHtml(category.title || "Категория")}
|
||||
</button>
|
||||
<div class="price-list-wrap" hidden>
|
||||
<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>
|
||||
${buildPriceTable(category.items)}
|
||||
</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;
|
||||
}
|
||||
|
||||
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 expanded = target.getAttribute("aria-expanded") === "true";
|
||||
target.setAttribute("aria-expanded", String(!expanded));
|
||||
listWrap.hidden = expanded;
|
||||
const toggle = target.closest(
|
||||
".price-category__toggle, .price-subcategory__toggle"
|
||||
);
|
||||
if (!toggle || !(toggle instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const listWrap = toggle.nextElementSibling;
|
||||
if (!(listWrap instanceof HTMLElement) || !listWrap.classList.contains("price-list-wrap")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const expanded = toggle.getAttribute("aria-expanded") === "true";
|
||||
toggle.setAttribute("aria-expanded", String(!expanded));
|
||||
listWrap.hidden = expanded;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user