1if (window.Swiper && !window.Scv3Swiper) { 2 window.Scv3Swiper = window.Swiper; 3} 4 5function scv3InitFilterBlocks() { 6 const blocks = document.querySelectorAll('.scv3-filter-block'); 7 if (!blocks.length) return; 8 9 blocks.forEach((block) => { 10 const toggle = block.querySelector('[data-scv3-filter-toggle]'); 11 const content = block.querySelector('[data-scv3-filter-content]'); 12 const more = block.querySelector('[data-scv3-filter-more]'); 13 const searchInput = block.querySelector('[data-scv3-filter-search]'); 14 const searchClear = block.querySelector('[data-scv3-filter-search-clear]'); 15 if (!toggle || !content) return; 16 const isFilterSheet = !!block.closest('[data-scv3-filter-sheet]'); 17 const isSidebarFilters = !!block.closest('.scv3-filters') 18 && !isFilterSheet 19 && !block.closest('[data-scv3-filter-all-sheet]'); 20 const isStatic = block.classList.contains('scv3-filter-block--static'); 21 const forceOpen = block.classList.contains('scv3-is-open'); 22 23 const rawLimit = parseInt(content.dataset.scv3FilterLimit || '0', 10); 24 const isSizesFilter = content.classList.contains('scv3-filter-list--sizes'); 25 const isDesktop = typeof window !== 'undefined' && 26 window.matchMedia && 27 window.matchMedia('(min-width: 769px)').matches; 28 const baseLimit = (isSizesFilter && isDesktop) ? Math.max(rawLimit, 10) : rawLimit; 29 const limit = isFilterSheet ? 0 : baseLimit; 30 const options = Array.from(content.querySelectorAll('[data-scv3-filter-option]')); 31 const canExpand = limit > 0 && options.length > limit; 32 const hasScroll = content.classList.contains('scv3-filter-list--scroll'); 33 const scrollOnExpand = content.hasAttribute('data-scv3-scroll-on-expand'); 34 let isExpanded = false; 35 let searchQuery = ''; 36 37 const selectedIdx = options.findIndex((opt) => { 38 return ( 39 (opt.querySelector('input[type="checkbox"]') || opt.querySelector('input[type="radio"]'))?.checked || 40 opt.classList.contains('scv3-is-current') 41 ); 42 }); 43 const hasPriceValue = content.classList.contains('scv3-filter-list--price') 44 ? Array.from(content.querySelectorAll('input')).some((inp) => (inp.value || '').trim() !== '') 45 : false; 46 if (!isSidebarFilters && selectedIdx >= 0 && canExpand && selectedIdx >= limit) { 47 isExpanded = true; // ÑаÑкÑÑваем, ÑÑÐ¾Ð±Ñ Ð²ÑбÑаннÑй ÑÐ»ÐµÐ¼ÐµÐ½Ñ Ð½Ðµ ÑкÑÑвалÑÑ 48 } 49 50 if (more && !canExpand) { 51 more.addEventListener('click', (e) => e.preventDefault()); 52 } 53 54 const normalize = (val) => String(val || '').toLowerCase().trim(); 55 const getOptionText = (opt) => { 56 if (opt.dataset.scv3FilterLabel) return opt.dataset.scv3FilterLabel; 57 const text = (opt.textContent || '').replace(/\s+/g, ' ').trim(); 58 opt.dataset.scv3FilterLabel = normalize(text); 59 return opt.dataset.scv3FilterLabel; 60 }; 61 62 const updateOptions = () => { 63 const searchActive = searchQuery !== ''; 64 const limitActive = canExpand && !isExpanded && !searchActive; 65 options.forEach((opt, idx) => { 66 const input = opt.querySelector('input[type="checkbox"], input[type="radio"]'); 67 const isSelected = (input && input.checked) || opt.classList.contains('scv3-is-current'); 68 const matches = !searchActive || getOptionText(opt).includes(searchQuery); 69 const hiddenBySearch = searchActive && !matches && !isSelected; 70 const hiddenByLimit = limitActive && idx >= limit; 71 const hidden = hiddenBySearch || hiddenByLimit; 72 opt.classList.toggle('scv3-filter-option--hidden', hiddenByLimit); 73 opt.classList.toggle('scv3-filter-option--search-hidden', hiddenBySearch); 74 opt.style.display = hidden ? 'none' : ''; 75 }); 76 if (hasScroll) { 77 if (scrollOnExpand) { 78 content.classList.toggle('scv3-filter-list--scroll-active', isExpanded); 79 } else { 80 content.classList.add('scv3-filter-list--scroll-active'); 81 } 82 } 83 if (more) { 84 more.classList.toggle('scv3-filter-more--expanded', isExpanded); 85 const labelEl = more.querySelector('[data-scv3-filter-more-label]'); 86 const expandLabel = more.getAttribute('data-label-expand') || 'ÐоказаÑÑ Ð²ÑÑ'; 87 const collapseLabel = more.getAttribute('data-label-collapse') || 'СкÑÑÑÑ'; 88 if (labelEl) { 89 labelEl.textContent = isExpanded ? collapseLabel : expandLabel; 90 } 91 } 92 }; 93 94 if (more && canExpand) { 95 more.addEventListener('click', (e) => { 96 e.preventDefault(); 97 isExpanded = !isExpanded; 98 updateOptions(); 99 }); 100 } 101 102 const setOpen = (isOpen) => { 103 block.classList.toggle('scv3-is-open', isOpen); 104 content.style.display = isOpen ? 'grid' : 'none'; 105 if (more) { 106 const searchActive = searchQuery !== ''; 107 const shouldShowMore = isOpen && !searchActive && 108 (canExpand || more.hasAttribute('data-label-expand')); 109 more.style.display = shouldShowMore ? 'inline-flex' : 'none'; 110 } 111 if (isOpen) { 112 updateOptions(); 113 } 114 }; 115 116 if (isStatic) { 117 setOpen(true); 118 } else {
119 const hasSelected = selectedIdx >= 0 || hasPriceValue; 120 setOpen(forceOpen || hasSelected); 121 } 122 123 if (searchInput) { 124 const applySearch = () => { 125 searchQuery = normalize(searchInput.value); 126 if (searchClear) { 127 searchClear.classList.toggle('scv3-is-visible', searchQuery !== ''); 128 } 129 updateOptions(); 130 if (more) { 131 const shouldShowMore = block.classList.contains('scv3-is-open') && 132 !searchQuery && (canExpand || more.hasAttribute('data-label-expand')); 133 more.style.display = shouldShowMore ? 'inline-flex' : 'none'; 134 } 135 }; 136 searchInput.addEventListener('input', applySearch); 137 searchInput.addEventListener('search', applySearch); 138 if (searchClear) { 139 searchClear.addEventListener('click', (e) => { 140 e.preventDefault(); 141 searchInput.value = ''; 142 searchInput.focus(); 143 applySearch(); 144 }); 145 } 146 applySearch(); 147 } 148 149 if (!isStatic) { 150 toggle.addEventListener('click', () => { 151 setOpen(!block.classList.contains('scv3-is-open')); 152 }); 153 } 154 }); 155} 156 157function scv3InitFilterColors(root = document) { 158 const nodes = root.querySelectorAll('.scv3-filter-list--colors [data-scv3-color]'); 159 if (!nodes.length) return; 160 161 const normalize = (val) => { 162 const raw = String(val || '').trim(); 163 const match = raw.match(/^#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/); 164 return match ? `#${match[1].toLowerCase()}` : ''; 165 }; 166 const toRgb = (hex) => { 167 const clean = hex.replace('#', ''); 168 if (clean.length === 3) { 169 return clean.split('').map((c) => parseInt(c + c, 16)); 170 } 171 return [clean.slice(0, 2), clean.slice(2, 4), clean.slice(4, 6)].map((c) => parseInt(c, 16)); 172 }; 173 const luminance = (r, g, b) => { 174 const srgb = [r, g, b].map((v) => { 175 const c = v / 255; 176 return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); 177 }); 178 return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2]; 179 }; 180 const contrastColor = (hex) => { 181 const [r, g, b] = toRgb(hex); 182 const lum = luminance(r, g, b); 183 const whiteContrast = (1.05) / (lum + 0.05); 184 const blackContrast = (lum + 0.05) / 0.05; 185 return whiteContrast >= blackContrast ? '#ffffff' : '#171717'; 186 }; 187 const isWhite = (hex) => { 188 const clean = hex.toLowerCase(); 189 return clean === '#fff' || clean === '#ffffff'; 190 }; 191 const whiteBorderColor = '#cfcfcf'; 192 193 nodes.forEach((label) => { 194 const raw = label.getAttribute('data-scv3-color') || ''; 195 const color = normalize(raw); 196 if (!color) return; 197 const contrast = contrastColor(color); 198 label.style.setProperty('--scv3-filter-color', color); 199 label.style.setProperty('--scv3-filter-color-border', isWhite(color) ? whiteBorderColor : 'transparent'); 200 label.style.setProperty('--scv3-filter-color-contrast', contrast); 201 }); 202} 203(function () { 204 const categorySliderEl = document.querySelector('.scv3-category-chips'); 205 const categoryPrev = document.querySelector('.scv3-slider-btn_prev'); 206 const categoryNext = document.querySelector('.scv3-slider-btn_next'); 207 const SwiperCtor = window.Scv3Swiper || window.Swiper; 208 209 const enhanceProductThumbs = () => { 210 const cards = document.querySelectorAll('.scv3-product-card'); 211 cards.forEach((card) => { 212 const thumb = card.querySelector('.scv3-product-card__thumb'); 213 if (!thumb || thumb.dataset.scv3SkipEnhance || thumb.querySelector('[data-scv3-card-swiper]')) return; 214 215 const mainImg = thumb.querySelector('img'); 216 const variantThumbImgs = Array.from(card.querySelectorAll('.scv3-variant-thumb img')); 217 const images = []; 218 219 if (mainImg && mainImg.src) { 220 images.push({ src: mainImg.src, alt: mainImg.alt || '' }); 221 } 222 variantThumbImgs.forEach((img) => { 223 if (img && img.src) { 224 images.push({ src: img.src, alt: img.alt || (mainImg ? mainImg.alt : '') }); 225 } 226 }); 227 228 const uniq = []; 229 const seen = new Set(); 230 images.forEach((img) => { 231 const key = (img.src || '').trim(); 232 if (!key || seen.has(key)) return; 233 seen.add(key); 234 uniq.push(img); 235 }); 236 237 if (uniq.length <= 1) return; 238 239 const favoriteBtn = thumb.querySelector('.scv3-product-card__favorite'); 240 if (favoriteBtn) { 241 thumb.removeChild(favoriteBtn); 242 } 243 thumb.innerHTML = ''; 244 if (favoriteBtn) { 245 thumb.appendChild(favoriteBtn); 246 } 247 248 const swiperEl = document.createElement('div'); 249 swiperEl.className = 'scv3-card-swiper swiper'; 250 swiperEl.setAttribute('data-scv3-card-swiper', ''); 251 252 const wrapper = document.createElement('div'); 253 wrapper.className = 'swiper-wrapper'; 254 255 uniq.forEach(({ src, alt }) => { 256 const slide = document.createElement('div'); 257 slide.className = 'swiper-slide'; 258 const imgEl = document.createElement('img'); 259 imgEl.src = src; 260 imgEl.alt = alt; 261 slide.appendChild(imgEl); 262 wrapper.appendChild(slide); 263 }); 264 265 const pagination = document.createElement('div'); 266 pagination.className = 'scv3-card-swiper__pagination'; 267 268 swiperEl.appendChild(wrapper); 269 swiperEl.appendChild(pagination); 270 271 thumb.appendChild(swiperEl); 272 273 const offset = mainImg && mainImg.src ? 1 : 0;
274 variantThumbImgs.forEach((img, idx) => { 275 img.dataset.scv3SlideIndex = offset + idx; 276 }); 277 }); 278 }; 279 280 const initCategoryFallback = () => { 281 if (!categorySliderEl) return; 282 const track = categorySliderEl.querySelector('.swiper-wrapper'); 283 if (!track) return; 284 285 categorySliderEl.classList.add('scv3-slider--fallback'); 286 const getPxVar = (name, fallback) => { 287 const val = getComputedStyle(categorySliderEl).getPropertyValue(name); 288 const num = parseInt(val, 10); 289 return Number.isFinite(num) ? num : fallback; 290 }; 291 const STEP = 292 getPxVar('--scv3-chip-width', 140) + getPxVar('--scv3-chip-gap', 14); 293 294 const updateButtons = () => { 295 const overflow = track.scrollWidth > categorySliderEl.clientWidth + 2; 296 if (categoryPrev && categoryNext) { 297 categoryPrev.style.display = overflow ? 'grid' : 'none'; 298 categoryNext.style.display = overflow ? 'grid' : 'none'; 299 } 300 if (!overflow || !categoryPrev || !categoryNext) return; 301 const maxScroll = track.scrollWidth - categorySliderEl.clientWidth - 2; 302 categoryPrev.disabled = categorySliderEl.scrollLeft <= 2; 303 categoryNext.disabled = categorySliderEl.scrollLeft >= maxScroll; 304 }; 305 306 const scrollByStep = (dir) => { 307 categorySliderEl.scrollBy({ left: STEP * dir, behavior: 'smooth' }); 308 }; 309 310 if (categoryPrev && categoryNext) { 311 categoryPrev.addEventListener('click', () => scrollByStep(-1)); 312 categoryNext.addEventListener('click', () => scrollByStep(1)); 313 } 314 315 categorySliderEl.addEventListener('scroll', updateButtons); 316 window.addEventListener('resize', updateButtons); 317 318 categorySliderEl.addEventListener( 319 'wheel', 320 (e) => { 321 if (Math.abs(e.deltaX) < Math.abs(e.deltaY)) { 322 categorySliderEl.scrollLeft += e.deltaY; 323 e.preventDefault(); 324 } 325 }, 326 { passive: false } 327 ); 328 329 updateButtons(); 330 }; 331 332 const initCategorySlider = () => { 333 if (!categorySliderEl || !categoryPrev || !categoryNext) return; 334 if (SwiperCtor) { 335 if (!window.Scv3Swiper) { 336 window.Scv3Swiper = SwiperCtor; 337 } 338 new SwiperCtor(categorySliderEl, { 339 slidesPerView: 'auto', 340 spaceBetween: 14, 341 watchOverflow: true, 342 navigation: { 343 prevEl: categoryPrev, 344 nextEl: categoryNext, 345 }, 346 }); 347 } else { 348 initCategoryFallback(); 349 } 350 }; 351 352 const initProductFallback = () => { 353 const swipers = document.querySelectorAll('[data-scv3-card-swiper]'); 354 swipers.forEach((swiperEl) => { 355 if (swiperEl.dataset.scv3SwiperInit) return; 356 const slides = swiperEl.querySelectorAll('.swiper-slide'); 357 if (slides.length <= 1) return; 358 swiperEl.classList.add('scv3-card-swiper_fallback'); 359 const paginationEl = 360 swiperEl.querySelector('.scv3-card-swiper__pagination') || 361 swiperEl.appendChild(document.createElement('div')); 362 paginationEl.classList.add('scv3-card-swiper__pagination'); 363 364 if (!paginationEl.children.length) { 365 slides.forEach((_, idx) => { 366 const dot = document.createElement('button'); 367 dot.type = 'button'; 368 dot.className = 'scv3-card-swiper__bullet'; 369 paginationEl.appendChild(dot); 370 }); 371 } 372 373 const bullets = Array.from(paginationEl.querySelectorAll('.scv3-card-swiper__bullet')); 374 let active = 0; 375 const setActive = (idx) => { 376 active = (idx + slides.length) % slides.length; 377 slides.forEach((slide, i) => { 378 slide.classList.toggle('scv3-is-active', i === active); 379 }); 380 bullets.forEach((b, i) => 381 b.classList.toggle('scv3-card-swiper__bullet--active', i === active) 382 ); 383 }; 384 385 bullets.forEach((bullet, idx) => { 386 bullet.addEventListener('click', () => setActive(idx)); 387 bullet.addEventListener('mouseover', () => setActive(idx)); 388 }); 389 390 const card = swiperEl.closest('.scv3-product-card'); 391 if (card) { 392 const thumbImgs = card.querySelectorAll('.scv3-variant-thumb img'); 393 } 394 395 setActive(0); 396 swiperEl.dataset.scv3SwiperInit = '1'; 397 }); 398 }; 399 400 const initProductSwipers = () => { 401 if (!SwiperCtor) { 402 initProductFallback(); 403 return; 404 } 405 406 const productSwipers = document.querySelectorAll('[data-scv3-card-swiper]'); 407 productSwipers.forEach((swiperEl) => { 408 if (swiperEl.dataset.scv3SwiperInit) return; 409 const paginationEl = swiperEl.querySelector('.scv3-card-swiper__pagination'); 410 const config = { 411 slidesPerView: 1, 412 loop: swiperEl.querySelectorAll('.swiper-slide').length > 1, 413 watchOverflow: true, 414 }; 415 if (paginationEl) { 416 config.pagination = { 417 el: paginationEl, 418 clickable: true, 419 bulletClass: 'scv3-card-swiper__bullet', 420 bulletActiveClass: 'scv3-card-swiper__bullet--active', 421 }; 422 } 423 424 const swiper = new SwiperCtor(swiperEl, config); 425 if (paginationEl) { 426 paginationEl.addEventListener('mouseover', (event) => { 427 const bullet = event.target.closest('.scv3-card-swiper__bullet'); 428 if (!bullet) return; 429 const bullets = Array.from(paginationEl.querySelectorAll('.scv3-card-swiper__bullet')); 430 const idx = bullets.indexOf(bullet); 431 if (idx >= 0) { 432 swiper.slideToLoop ? swiper.slideToLoop(idx) : swiper.slideTo(idx); 433 } 434 }); 435 } 436 swiperEl.dataset.scv3SwiperInit = '1'; 437 }); 438 }; 439 440 window.scv3InitProductSliders = () => { 441 initProductSwipers(); 442 }; 443 444 enhanceProductThumbs(); 445 initCategorySlider(); 446 initProductSwipers(); 447})(); 448 449const Page = { 450 state: { 451 currentPage: 1, 452 basePath: "",
453 urlParams: "" 454 }, 455 456 _selectors: { 457 hideInputs: { 458 count_products_in_html: "#countProductsInHTML" 459 }, 460 461 header_p_count: ".header-page__count", //СелекÑÐ¾Ñ ÑлеменÑа Ñ ÐºÐ¾Ð»-во ÑоваÑов на ÑайÑе todo: вÑнеÑÑи в catalog-v3.php 462 463 cards: { 464 variantEl: '.scv3-variant-thumb', 465 productsBlock: '.scv3-product-grid' 466 }, 467 468 loadMoreBtn: ".scv3-load-more", 469 pagination: ".scv3-pagination", 470 termAdminInfo: "[data-scv3-term-admin-info]", 471 472 filters: { 473 categories: '[data-scv3-filter-slug="categories"] .scv3-filter-plain__item', 474 option: ".scv3-filter-option", 475 priceInput: ".scv3-price-input", 476 sortOption: ".scv3-sort-dropdown__option" 477 } 478 }, 479 _state: { 480 currentPage: 1, 481 uiPage: 1, 482 basePath: '', 483 urlParams: {}, 484 perPage: 24, 485 total: 0, 486 totalPages: 1, 487 filterDebounce: 600, 488 filterTimer: null, 489 categories: [], 490 activeCategorySlug: '', 491 categoryTreeExpanded: {}, 492 productsById: {}, 493 filterEdit: null, 494 landingContext: null 495 }, 496 497 498 499 preload() { 500 let res = false; 501 502 //ÐеÑем кол-во ÑоваÑов и ÑобобÑажаем в Ñапке 503 res = this.mainBlocks.getCountProductsInHTML(); 504 505 if(this.helpers.isNumeric(res)) 506 this.helpers.setHeaderCount(res); 507 508 this._state = { ...this._state, ...this.helpers.getRouteState() }; 509 this._state.landingContext = this.helpers.normalizeLandingContext(window.scv3LandingContext || null); 510 if(this.helpers.isNumeric(res)) { 511 this._state.total = Number(res); 512 this._state.totalPages = Math.max(1, Math.ceil(this._state.total / this._state.perPage)); 513 } 514 this._state.uiPage = this._state.currentPage || 1; 515 this.helpers.ensurePagination(); 516 this.helpers.updateSliderVisibility(); 517 518 const initialProducts = (typeof products !== 'undefined') ? products : null; 519 const initialItems = Array.isArray(initialProducts?.items) 520 ? initialProducts.items 521 : (Array.isArray(initialProducts) ? initialProducts : []); 522 if (initialItems.length) { 523 this.helpers.cacheProducts(initialItems, { reset: true }); 524 } 525 526 this.helpers.syncFilterInputsFromUrl(); 527 this.helpers.initCategoryTrees(); 528 scv3InitFilterBlocks(); 529 scv3InitFilterColors(); 530 531 this.staticLisaners(); 532 this.dynamicLisaners.cardVariants(); 533 this.dynamicLisaners.pagination(); 534 this.dynamicLisaners.categories(); 535 this.dynamicLisaners.categoriesTree(); 536 this.dynamicLisaners.options(); 537 this.dynamicLisaners.price(); 538 this.dynamicLisaners.sorting(); 539 }, 540 541 //Раз и навÑегда поÑле загÑÑзки ÑÑÑ 542 staticLisaners() { 543 544 $(Page._selectors.loadMoreBtn).off().on('click', async function(e){ 545 e.preventDefault(); 546 547 Page._state.currentPage += 1; 548 const nextPage = Page._state.currentPage; 549 Page.helpers.fetchAndRender({ 550 page: nextPage, 551 append: true, 552 withFacets: false, 553 updateUrl: false, 554 updatePagination: false 555 }); 556 }); 557 558 $(document).off('click.scv3LandingLinks').on('click.scv3LandingLinks', '[data-scv3-landing-link]', function(e){ 559 if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) { 560 return; 561 } 562 if (this.target && this.target.toLowerCase() !== '_self') { 563 return; 564 } 565 566 let targetUrl; 567 try { 568 targetUrl = new URL(this.href, location.origin); 569 } catch (err) { 570 return; 571 } 572 if (targetUrl.origin !== location.origin) { 573 return; 574 } 575 576 const context = Page.helpers.normalizeLandingContext(Page._state.landingContext); 577 const targetPath = targetUrl.pathname.replace(/^\/+|\/+$/g, ''); 578 const landing = context?.pages?.find((page) => { 579 const pagePath = String(page?.url || '').replace(/^\/+|\/+$/g, ''); 580 return `${context.basePath}/${pagePath}`.replace(/^\/+|\/+$/g, '') === targetPath; 581 }); 582 if (!landing) { 583 return; 584 } 585 586 e.preventDefault(); 587 if (Page._state.filterTimer) { 588 clearTimeout(Page._state.filterTimer); 589 Page._state.filterTimer = null; 590 } 591 592 const params = {}; 593 targetUrl.searchParams.forEach((value, rawKey) => { 594 const key = Page.helpers.normalizeFilterParamName(rawKey); 595 if (params[key] === undefined) { 596 params[key] = value; 597 } else if (Array.isArray(params[key])) { 598 params[key].push(value); 599 } else { 600 params[key] = [params[key], value]; 601 } 602 }); 603 Object.entries(landing.attributes || {}
603).forEach(([slug, rawValues]) => { 604 const values = (Array.isArray(rawValues) ? rawValues : [rawValues]) 605 .map((value) => String(value)) 606 .filter(Boolean); 607 if (values.length) { 608 params[`pa-${slug}`] = values.length === 1 ? values[0] : values; 609 } 610 }); 611 612 Page._state.basePath = context.basePath; 613 Page._state.urlParams = Page.helpers.normalizeUrlParams(params); 614 Page._state.currentPage = 1; 615 Page._state.uiPage = 1; 616 Page.helpers.fetchAndRender({ 617 page: 1, 618 append: false, 619 withFacets: true, 620 history: 'push' 621 }); 622 Page.helpers.closeMobileFilters(); 623 }); 624 }, 625 626 //ÐоÑле ÑеÑендеÑа пеÑевеÑиваем 627 dynamicLisaners: { 628 categories(){ 629 $(document).off('click.scv3Categories').on('click.scv3Categories', Page._selectors.filters.categories, async function(e){ 630 e.preventDefault(); 631 632 const href = $(this).attr('href') || ''; 633 if (!href || href === '#') return; 634 635 // href вида /cat/muzhskoe/jersey/ â менÑем basePath и ÑбÑаÑÑваем ÑÑÑаниÑÑ Ð½Ð° 1 636 let path = ''; 637 try { 638 const parsed = new URL(href, location.origin); 639 path = parsed.pathname || ''; 640 } catch (err) { 641 path = (href.split('?')[0] || ''); 642 } 643 path = path.replace(/^\/+|\/+$/g, ''); 644 645 // /cat/ без Ñлага â ÑÑо главнаÑ, пÑиводим к /shop/ 646 if (!path || /^cat\/?$/.test(path)) { 647 path = 'shop'; 648 } 649 650 Page._state.basePath = path; 651 Page._state.landingContext = null; 652 Page._state.currentPage = 1; 653 Page._state.urlParams = {}; // Ñмена каÑегоÑии â ÑбÑаÑÑваем вÑе акÑивнÑе ÑилÑÑÑÑ 654 Page.helpers.updateUrl(1, 'push'); 655 Page.helpers.updateSliderVisibility(); 656 657 Page.mainBlocks.runFilters(); 658 Page.helpers.closeMobileFilters(); 659 660 }); 661 }, 662 categoriesTree() { 663 if (Page._state.categoryTreeToggleBound) { 664 return; 665 } 666 Page._state.categoryTreeToggleBound = true; 667 document.addEventListener('click', (e) => { 668 const btn = e.target && e.target.closest ? e.target.closest('[data-scv3-cat-toggle]') : null; 669 if (!btn) return; 670 e.preventDefault(); 671 e.stopPropagation(); 672 const row = btn.closest('[data-scv3-cat-row]'); 673 if (!row) return; 674 Page.helpers.toggleCategoryTreeRow(row); 675 }, true); 676 }, 677 678 options(){ 679 const selector = `${Page._selectors.filters.option} input[type="checkbox"]`; 680 $(document).off('change.scv3Filters').on('change.scv3Filters', selector, function(){ 681 const isEdit = Page.helpers.isFilterEditMode && Page.helpers.isFilterEditMode(); 682 const $input = $(this); 683 const $label = $input.closest(Page._selectors.filters.option); 684 if ($label.length) { 685 $label.toggleClass('scv3-is-checked', $input.prop('checked')); 686 } 687 const name = Page.helpers.normalizeFilterParamName($input.attr('name') || ''); 688 const value = $input.val(); 689 if (!name || value === undefined) return; 690 const isCategoryTreeInput = $input.is('[data-scv3-cat-checkbox]'); 691 if (isCategoryTreeInput) { 692 Page.helpers.applyCategoryTreeCascade($input.get(0)); 693 } 694 695 const paramsSource = Page.helpers.normalizeUrlParams( 696 Page.helpers.getFilterEditParams ? Page.helpers.getFilterEditParams() : Page._state.urlParams 697 ); 698 const usp = new URLSearchParams(Page.helpers.serializeUrlParams(paramsSource)); 699 700 if (isCategoryTreeInput) { 701 const selected = []; 702 const root = $input.closest('[data-scv3-filter-slug="categories"]'); 703 root.find('input[type="checkbox"]').filter(function () { 704 return Page.helpers.normalizeFilterParamName(this.getAttribute('name') || '') === name && this.checked; 705 }).each(function () { 706 const v = String($(this).val() ?? '').trim(); 707 if (!v) return; 708 selected.push(v); 709 }); 710 usp.delete(name); 711 selected.forEach((v) => usp.append(name, v)); 712 } else { 713 const currentVals = usp.getAll(name).filter((v) => v !== ''); 714 const isChecked = $input.prop('checked'); 715 if (isChecked) { 716 if (!currentVals.includes(String(value))) { 717 usp.append(name, value); 718 } 719 } else { 720 const remaining = currentVals.filter((v) => v !== String(value)); 721 usp.delete(name); 722 remaining.forEach((v) => usp.append(name, v)); 723 } 724 } 725 726 const newParams = {};
727 usp.forEach((v, k) => { 728 if (newParams[k]) { 729 if (Array.isArray(newParams[k])) { 730 newParams[k].push(v); 731 } else { 732 newParams[k] = [newParams[k], v]; 733 } 734 } else { 735 newParams[k] = v; 736 } 737 }); 738 if (isEdit) { 739 Page.helpers.setFilterEditParams(newParams); 740 return; 741 } 742 Page._state.urlParams = newParams; 743 Page._state.currentPage = 1; 744 Page.helpers.updateUrl(1); 745 746 if (Page._state.filterTimer) { 747 clearTimeout(Page._state.filterTimer); 748 } 749 Page._state.filterTimer = setTimeout(() => { 750 Page._state.filterTimer = null; 751 Page.mainBlocks.runFilters(); 752 }, Page._state.filterDebounce || 600); 753 754 if (typeof window.scv3UpdateMobileFilterPills === 'function') { 755 window.scv3UpdateMobileFilterPills(); 756 } 757 }); 758 759 $(selector).each(function(){ 760 const $input = $(this); 761 const $label = $input.closest(Page._selectors.filters.option); 762 if ($label.length) { 763 $label.toggleClass('scv3-is-checked', $input.prop('checked')); 764 } 765 }); 766 }, 767 768 price() { 769 const selector = Page._selectors.filters.priceInput; 770 const normalizeName = (name) => { 771 const raw = String(name || ''); 772 return raw.endsWith('[]') ? raw.slice(0, -2) : raw; 773 }; 774 const cleanVal = (val) => { 775 const str = String(val || '').replace(/[^0-9]/g, ''); 776 return str === '' ? '' : String(parseInt(str, 10)); 777 }; 778 const formatVal = (val) => { 779 const digits = cleanVal(val); 780 if (!digits) return ''; 781 return Page.helpers.formatPriceDisplay(digits) || digits; 782 }; 783 const setCaretToEnd = (el) => { 784 if (!el || typeof el.setSelectionRange !== 'function') return; 785 const len = el.value.length; 786 try { 787 el.setSelectionRange(len, len); 788 } catch (err) { 789 // ignore selection errors for unsupported input types 790 } 791 }; 792 const schedule = (immediate = false) => { 793 if (Page._state.filterTimer) { 794 clearTimeout(Page._state.filterTimer); 795 Page._state.filterTimer = null; 796 } 797 if (immediate) { 798 Page.mainBlocks.runFilters(); 799 return; 800 } 801 Page._state.filterTimer = setTimeout(() => { 802 Page._state.filterTimer = null; 803 Page.mainBlocks.runFilters(); 804 }, Page._state.filterDebounce || 600); 805 }; 806 const isInSheet = ($input) => { 807 const node = $input && $input.length ? $input.get(0) : null; 808 if (!node) return false; 809 return !!node.closest('[data-scv3-filter-sheet], [data-scv3-filter-all-sheet]'); 810 }; 811 const applyFromBlock = ($input, immediate = false) => { 812 const $list = $input.closest('.scv3-filter-list--price'); 813 const inputs = $list.length ? $list.find(selector) : $(selector); 814 const isEdit = Page.helpers.isFilterEditMode && Page.helpers.isFilterEditMode(); 815 const paramsSource = Page.helpers.getFilterEditParams ? Page.helpers.getFilterEditParams() : Page._state.urlParams; 816 const params = { ...paramsSource }; 817 818 inputs.each(function(){ 819 const $el = $(this); 820 const rawName = $el.attr('name') || ''; 821 const name = normalizeName(rawName); 822 if (!name) return; 823 if (rawName && rawName !== name) { 824 delete params[rawName]; 825 } 826 const cleaned = cleanVal($el.val()); 827 if (cleaned !== '') { 828 params[name] = cleaned; 829 $el.val(formatVal(cleaned)); 830 } else { 831 delete params[name]; 832 $el.val(''); 833 } 834 if ($input.length && $el.get(0) === $input.get(0)) { 835 setCaretToEnd($el.get(0)); 836 } 837 }); 838 839 if (isEdit) { 840 Page.helpers.setFilterEditParams(params); 841 return; 842 } 843 Page._state.urlParams = params; 844 Page._state.currentPage = 1; 845 Page.helpers.updateUrl(1); 846 if (typeof window.scv3UpdateMobileFilterPills === 'function') { 847 window.scv3UpdateMobileFilterPills(); 848 } 849 schedule(immediate); 850 if (!isInSheet($input)) { 851 Page.helpers.closeMobileFilters(); 852 } 853 }; 854 855 $(document) 856 .off('input.scv3Price', selector) 857 .on('input.scv3Price', selector, function(){ 858 const $input = $(this); 859 if (isInSheet($input)) return; 860 applyFromBlock($input, false); 861 }); 862 863 $(document) 864 .off('change.scv3Price', selector) 865 .on('change.scv3Price', selector, function(){ 866 applyFromBlock($(this), false); 867 }); 868 869 $(document) 870 .off('keydown.scv3Price', selector) 871 .on('keydown.scv3Price', selector, function(e){ 872 if (e.key === 'Enter') { 873 e.preventDefault(); 874 applyFromBlock($(this), true); 875 } 876 }); 877 }, 878 879 sorting() { 880 const selector = Page._selectors.filters.sortOption; 881 const dropdownSelector = '.scv3-sort-dropdown'; 882 const normalizeSort = (val) => { 883 if (!val) return ''; 884 const v = String(val).trim(); 885 if (v === 'popular') return 'popularity'; 886 return v; 887 }; 888 const setActive = (key, label, $option) => { 889 const $dropdown = $option.closest('.scv3-sort-dropdown'); 890 if ($dropdown.length) { 891 $dropdown.find(selector).removeClass('scv3-is-active'); 892 $option.addClass('scv3-is-active'); 893 const summary = $dropdown.find('summary').get(0); 894 if (summary) { 895 const labelEl = summary.querySelector('.scv3-sort-dropdown__label'); 896 if (labelEl) { 897 labelEl.textContent = label || key || ''; 898 } else { 899 summary.textContent = label || key || ''; 900 } 901 } 902 const detailsEl = $dropdown.get(0); 903 if (detailsEl) { 904 detailsEl.removeAttribute('open'); 905 } 906 } 907 }; 908 909 $(document).off('click.scv3Sort', selector).on('click.scv3Sort', selector, function(e){ 910 e.preventDefault(); 911 const $opt = $(this); 912 const rawSort = $opt.data('sort') || $opt.attr('data-sort-key') || $opt.attr('data-sort') || ''; 913 const label = $opt.text().trim(); 914 const sort = normalizeSort(rawSort || label); 915 if (!sort) return; 916
917 const params = { ...Page._state.urlParams }; 918 params.sort = sort; 919 Page._state.urlParams = params; 920 Page._state.currentPage = 1; 921 Page.helpers.updateUrl(1); 922 setActive(sort, label, $opt); 923 Page.helpers.fetchAndRender({ 924 page: 1, 925 append: false, 926 withFacets: false 927 }); 928 Page.helpers.closeMobileFilters(); 929 }); 930 931 $(document).off('click.scv3SortOutside').on('click.scv3SortOutside', function(e){ 932 const $target = $(e.target); 933 if ($target.closest(dropdownSelector).length) return; 934 $(dropdownSelector).removeAttr('open'); 935 }); 936 937 const currentSort = normalizeSort(Page._state.urlParams.sort || ''); 938 const $opts = $(selector); 939 if (currentSort && $opts.length) { 940 const $activeOpt = $opts.filter(`[data-sort="${currentSort}"], [data-sort-key="${currentSort}"]`).first(); 941 if ($activeOpt.length) { 942 setActive(currentSort, $activeOpt.text().trim(), $activeOpt); 943 } 944 } 945 }, 946 947 cardVariants(){ 948 const selector = Page._selectors.cards.variantEl; 949 const $variants = $(selector); 950 951 $variants.off().on('click', function(){ 952 const variantId = Number($(this).data('variantId')) || 0; 953 if (!variantId) return; 954 955 const $card = $(this).closest('article'); 956 const prevVariantsTrack = $card.length 957 ? $card.find('.scv3-product-card__variants').get(0) 958 : null; 959 const prevVariantsScrollLeft = prevVariantsTrack ? prevVariantsTrack.scrollLeft : 0; 960 const prevThumbData = $card.length 961 ? $card.find(Page._selectors.cards.variantEl).map((_, el) => { 962 const $el = $(el); 963 const id = Number($el.data('variantId')) || 0; 964 const $img = $el.find('img').first(); 965 return { 966 id, 967 src: ($img.attr('src') || '').trim(), 968 alt: ($img.attr('alt') || '').trim() 969 }; 970 }).get().filter((item) => item.id > 0 && item.src !== '') 971 : []; 972 const domOrder = $card.length 973 ? $card.find(Page._selectors.cards.variantEl) 974 .map((_, el) => Number($(el).data('variantId')) || 0) 975 .get() 976 .filter((id) => id > 0) 977 : []; 978 979 const cached = Page.helpers.getCachedProduct(variantId); 980 if (!cached) return; 981 982 const cachedOrder = Array.isArray(cached.variant_order) ? cached.variant_order : []; 983 const variantOrder = domOrder.length ? domOrder : cachedOrder; 984 const renderData = variantOrder.length ? { ...cached, variant_order: variantOrder } : cached; 985 const cardHtml = Render.Cards.card(renderData); 986 987 if (cardHtml) { 988 const $newCard = $(cardHtml); 989 const newTrackBeforeInsert = $newCard.find('.scv3-product-card__variants').get(0); 990 if (newTrackBeforeInsert) { 991 newTrackBeforeInsert.style.scrollBehavior = 'auto'; 992 newTrackBeforeInsert.scrollLeft = prevVariantsScrollLeft; 993 newTrackBeforeInsert.dataset.scv3RestoreScrollLeft = String(prevVariantsScrollLeft || 0); 994 } 995 $card.replaceWith($newCard); 996 const $variantsWrap = $newCard.find('.scv3-product-card__variants').first(); 997 if ($variantsWrap.length && domOrder.length) { 998 const existingIds = new Set( 999 $variantsWrap.find(Page._selectors.cards.variantEl) 1000 .map((_, el) => Number(el.getAttribute('data-variant-id') || 0)) 1001 .get() 1002 .filter((id) => id > 0) 1003 ); 1004 prevThumbData.forEach((item) => { 1005 if (existingIds.has(item.id)) return; 1006 const node = document.createElement('div'); 1007 node.className = 'scv3-variant-thumb'; 1008 node.setAttribute('data-variant-id', String(item.id)); 1009 const img = document.createElement('img'); 1010 img.src = item.src; 1011 img.alt = item.alt; 1012 node.appendChild(img); 1013 $variantsWrap.append(node); 1014 existingIds.add(item.id); 1015 }); 1016 1017 const nodes = $variantsWrap.find(Page._selectors.cards.variantEl).get(); 1018 const byId = new Map(); 1019 nodes.forEach((node) => { 1020 const id = Number(node.getAttribute('data-variant-id') || 0); 1021 if (id > 0 && !byId.has(id)) { 1022 byId.set(id, node); 1023 } 1024 }); 1025 1026 const orderedNodes = []; 1027 domOrder.forEach((id) => { 1028 const node = byId.get(id); 1029 if (node) { 1030 orderedNodes.push(node); 1031 byId.delete(id); 1032 } 1033 });
1034 byId.forEach((node) => orderedNodes.push(node)); 1035 if (orderedNodes.length) { 1036 orderedNodes.forEach((node) => { 1037 $variantsWrap.append(node); 1038 }); 1039 } 1040 } 1041 1042 const $thumbs = $newCard.find(Page._selectors.cards.variantEl); 1043 if ($thumbs.length) { 1044 const $active = $thumbs.filter(`[data-variant-id="${variantId}"]`); 1045 if ($active.length) { 1046 $thumbs.removeClass('scv3-variant-thumb--active'); 1047 $active.addClass('scv3-variant-thumb--active'); 1048 } 1049 } 1050 Page.dynamicLisaners.cardVariants(); 1051 if (typeof window.scv3InitProductSliders === 'function') { 1052 window.scv3InitProductSliders(); 1053 } 1054 const trackAfterInit = $newCard.find('.scv3-product-card__variants').get(0); 1055 if (trackAfterInit) { 1056 const restoreValue = Number(trackAfterInit.dataset.scv3RestoreScrollLeft || prevVariantsScrollLeft || 0); 1057 trackAfterInit.scrollLeft = restoreValue; 1058 delete trackAfterInit.dataset.scv3RestoreScrollLeft; 1059 setTimeout(() => { 1060 trackAfterInit.style.scrollBehavior = ''; 1061 }, 0); 1062 } 1063 } 1064 }); 1065 1066 $variants.on('mouseenter', function(){ 1067 const $variant = $(this); 1068 const src = $variant.find('img').attr('src') || ''; 1069 if (!src) return; 1070 const $card = $variant.closest('article'); 1071 if (!$card.length) return; 1072 const imgEl = Page.helpers.getCardMainImageEl($card); 1073 if (!imgEl) return; 1074 1075 const $img = $(imgEl); 1076 if ($img.data('scv3OrigSrc') === undefined) { 1077 $img.data('scv3OrigSrc', imgEl.getAttribute('src') || ''); 1078 if (imgEl.hasAttribute('srcset')) { 1079 $img.data('scv3OrigSrcset', imgEl.getAttribute('srcset') || ''); 1080 } 1081 } 1082 1083 imgEl.setAttribute('src', src); 1084 if (imgEl.hasAttribute('srcset')) { 1085 imgEl.setAttribute('srcset', src); 1086 } 1087 1088 $variant.data('scv3HoverImg', imgEl); 1089 }); 1090 1091 $variants.on('mouseleave', function(){ 1092 const $variant = $(this); 1093 const imgEl = $variant.data('scv3HoverImg'); 1094 if (!imgEl) return; 1095 const $img = $(imgEl); 1096 const orig = $img.data('scv3OrigSrc'); 1097 if (orig !== undefined) { 1098 imgEl.setAttribute('src', orig); 1099 } 1100 const origSet = $img.data('scv3OrigSrcset'); 1101 if (origSet !== undefined) { 1102 if (origSet) { 1103 imgEl.setAttribute('srcset', origSet); 1104 } else { 1105 imgEl.removeAttribute('srcset'); 1106 } 1107 } 1108 $variant.removeData('scv3HoverImg'); 1109 }); 1110 }, 1111 1112 pagination() { 1113 $(Page._selectors.pagination).off().on('click', 'a', async function(e){ 1114 e.preventDefault(); 1115 const href = $(this).attr('href') || ''; 1116 if (!href || href === '#') return; 1117 1118 // ÐаÑÑим Ð½Ð¾Ð¼ÐµÑ ÑÑÑаниÑÑ Ð¸Ð· href (иÑем /page/N/ или query page). 1119 let page = 1; 1120 const match = href.match(/\/page\/(\d+)/); 1121 if (match && match[1]) { 1122 page = parseInt(match[1], 10); 1123 } else { 1124 const usp = new URLSearchParams(href.split('?')[1] || ''); 1125 if (usp.has('page')) { 1126 page = parseInt(usp.get('page'), 10); 1127 } 1128 } 1129 if (!Number.isFinite(page) || page < 1) page = 1; 1130 1131 Page.helpers.fetchAndRender({ 1132 page: page, 1133 append: false, 1134 withFacets: false, 1135 history: 'push' 1136 }); 1137 }); 1138 } 1139 }, 1140 1141 mainBlocks: { 1142 1143 runFilters() { 1144 Page._state.currentPage = 1; 1145 Page.helpers.fetchAndRender({ 1146 page: 1, 1147 append: false, 1148 withFacets: true 1149 }); 1150 }, 1151 1152 getCountProductsInHTML() { 1153 let el = $(Page._selectors.hideInputs.count_products_in_html); 1154 1155 if(el.length === 0) 1156 return false; 1157 1158 return el.val(); 1159 } 1160 }, 1161 1162 helpers: { 1163 getProductId(item = {}) { 1164 const dataId = Number(item?.data?.product_id || item?.data?.id || 0); 1165 const directId = Number(item?.product_id || item?.post_id || item?.id || 0); 1166 return dataId || directId || 0; 1167 }, 1168 1169 getCardMainImageEl(cardEl) { 1170 const el = cardEl && cardEl.jquery ? cardEl.get(0) : cardEl; 1171 if (!el) return null; 1172 const thumb = el.querySelector('.scv3-product-card__thumb'); 1173 if (!thumb) return null; 1174 return ( 1175 thumb.querySelector('.swiper-slide-active img') || 1176 thumb.querySelector('.swiper-slide img') || 1177 thumb.querySelector('img') 1178 ); 1179 }, 1180 1181 normalizeCachedItem(raw = {}, colorsGroup = []) { 1182 const item = { ...raw }; 1183 const id = Page.helpers.getProductId(item); 1184 const srcData = (item.data && typeof item.data === 'object') ? item.data : {}; 1185 const data = { ...srcData }; 1186 if (id) { 1187 data.product_id = id; 1188 } 1189 if (!data.title) data.title = item.title || ''; 1190 if (!data.name) data.name = item.name || ''; 1191 if (!data.thumbnail_img) data.thumbnail_img = item.thumbnail_img || ''; 1192 if (!data.product_url) data.product_url = item.product_url || item.url || ''; 1193 if (!data.img_sizes) data.img_sizes = item.img_sizes || ''; 1194 if (!data.thumbnail_img) { 1195 const gallery = Render?.Helpers?.normalizeGallery 1196 ? Render.Helpers.normalizeGallery(data.gallery_imgs || data.galleryImgs) 1197 : []; 1198 if (gallery.length) data.thumbnail_img = gallery[0]; 1199 } 1200 item.data = data; 1201 if (!item.product_id && id) { 1202 item.product_id = id; 1203 } 1204 if (!Array.isArray(item.colors)) { 1205 item.colors = Array.isArray(colorsGroup) ? colorsGroup : []; 1206 } 1207 if (!item.post_id && id) { 1208 item.post_id = id; 1209 } 1210 return item; 1211 }, 1212 1213 buildSelfColor(item = {}) { 1214 const id = Page.helpers.getProductId(item); 1215 if (!id) return null; 1216 const data = (item.data && typeof item.data === 'object') ? item.data : {}; 1217 let thumbnail = data.thumbnail_img || item.thumbnail_img || ''; 1218 if (!thumbnail && Render?.Helpers?.normalizeGallery) { 1219 const gallery = Render.Helpers.normalizeGallery(data.gallery_imgs || data.galleryImgs); 1220 if (gallery.length) thumbnail = gallery[0]; 1221 } 1222 1223 return { 1224 product_id: id, 1225 title: data.title || item.title || '', 1226 name: data.name || item.name || '', 1227 product_url: data.product_url || item.product_url || item.url || '', 1228 thumbnail_img: thumbnail, 1229 img_sizes: data.img_sizes || item.img_sizes || '', 1230 data: data, 1231 min_regular_price: item.min_regular_price ?? 0, 1232 max_regular_price: item.max_regular_price ?? 0, 1233 min_sale_price: item.min_sale_price ?? 0, 1234 max_sale_price: item.max_sale_price ?? 0, 1235 min_price_effective: item.min_price_effective ?? 0, 1236 stock_status: item.stock_status ?? 0 1237 }; 1238 }, 1239 1240 mergeCachedProduct(existing = null, incoming = null) { 1241 if (!existing) return incoming; 1242 if (!incoming) return existing; 1243 const merged = { ...existing, ...incoming }; 1244 if (existing.data || incoming.data) { 1245 merged.data = { ...(existing.data || {}), ...(incoming.data || {}) }; 1246 } 1247 if (Array.isArray(incoming.colors) && incoming.colors.length) { 1248 merged.colors = incoming.colors; 1249 } 1250 return merged; 1251 }, 1252 1253 cacheProducts(items = [], { reset = false } = {}) { 1254 if (reset) { 1255 Page._state.productsById = {}; 1256 } 1257 const cache = Page._state.productsById; 1258 if (!Array.isArray(items)) return; 1259
1260 items.forEach((item) => { 1261 if (!item || typeof item !== 'object') return; 1262 const colorsRaw = Array.isArray(item.colors) ? item.colors : []; 1263 const colorsGroup = colorsRaw.slice(); 1264 const selfColor = Page.helpers.buildSelfColor(item); 1265 if (selfColor) { 1266 const selfId = Page.helpers.getProductId(selfColor); 1267 const hasSelf = colorsGroup.some((color) => Page.helpers.getProductId(color) === selfId); 1268 if (!hasSelf) { 1269 colorsGroup.push(selfColor); 1270 } 1271 } 1272 const itemId = Page.helpers.getProductId(item); 1273 if (itemId) { 1274 const normalized = Page.helpers.normalizeCachedItem(item, colorsGroup); 1275 cache[itemId] = Page.helpers.mergeCachedProduct(cache[itemId], normalized); 1276 } 1277 colorsGroup.forEach((color) => { 1278 if (!color || typeof color !== 'object') return; 1279 const colorId = Page.helpers.getProductId(color); 1280 if (!colorId) return; 1281 const colorItem = Page.helpers.normalizeCachedItem(color, colorsGroup); 1282 cache[colorId] = Page.helpers.mergeCachedProduct(cache[colorId], colorItem); 1283 }); 1284 }); 1285 }, 1286 1287 getCachedProduct(id) { 1288 const key = Number(id) || 0; 1289 return key ? (Page._state.productsById[key] || null) : null; 1290 }, 1291 1292 formatPriceDisplay(value) { 1293 const raw = String(value ?? '').trim(); 1294 if (!raw) return ''; 1295 const digits = raw.replace(/[^0-9]/g, ''); 1296 if (!digits) return ''; 1297 const num = Number(digits); 1298 if (!Number.isFinite(num)) return ''; 1299 return num.toLocaleString('ru-RU'); 1300 }, 1301 1302 normalizePriceRange(priceRange = null) { 1303 const extract = (src) => { 1304 if (!src || typeof src !== 'object') return {}; 1305 if (src.value && typeof src.value === 'object') return extract(src.value); 1306 return src; 1307 }; 1308 const value = extract(priceRange); 1309 const selected = extract(priceRange && priceRange.selected ? priceRange.selected : null); 1310 return { 1311 min: value.min ?? value.from ?? null, 1312 max: value.max ?? value.to ?? null, 1313 selected_from: selected.from ?? selected.min ?? null, 1314 selected_to: selected.to ?? selected.max ?? null 1315 }; 1316 }, 1317 syncFilterInputsFromUrl() { 1318 const filters = document.querySelector('.scv3-filters'); 1319 if (!filters) return; 1320 1321 const params = Page._state.urlParams || {}; 1322 const selectedMap = {}; 1323 Object.entries(params).forEach(([key, val]) => { 1324 if (Array.isArray(val)) { 1325 selectedMap[key] = val.map((item) => String(item)); 1326 } else if (val !== null && val !== undefined) { 1327 selectedMap[key] = [String(val)]; 1328 } 1329 }); 1330 1331 const getValues = (name) => { 1332 if (!name) return []; 1333 if (selectedMap[name]) return selectedMap[name]; 1334 if (name.endsWith('[]')) { 1335 return selectedMap[name.slice(0, -2)] || []; 1336 } 1337 return selectedMap[`${name}[]`] || []; 1338 }; 1339 1340 const checkboxes = filters.querySelectorAll('.scv3-filter-option input[type="checkbox"]'); 1341 checkboxes.forEach((input) => { 1342 const name = input.getAttribute('name') || ''; 1343 const values = getValues(name); 1344 const val = input.value !== undefined ? String(input.value) : ''; 1345 const shouldCheck = val !== '' && values.includes(val); 1346 input.checked = shouldCheck; 1347 const label = input.closest('.scv3-filter-option'); 1348 if (label) { 1349 label.classList.toggle('scv3-is-checked', shouldCheck); 1350 } 1351 }); 1352 1353 const priceInputs = filters.querySelectorAll('.scv3-filter-list--price input'); 1354 priceInputs.forEach((input) => { 1355 const name = input.getAttribute('name') || ''; 1356 const values = getValues(name); 1357 const raw = values[0] !== undefined ? String(values[0]) : ''; 1358 if (!raw) { 1359 input.value = ''; 1360 return; 1361 } 1362 const formatted = Page.helpers.formatPriceDisplay(raw); 1363 input.value = formatted || raw; 1364 }); 1365 Page.helpers.initCategoryTrees(filters); 1366 }, 1367 initCategoryTrees(root = document) { 1368 if (!root || !root.querySelectorAll) return; 1369 const trees = root.querySelectorAll('[data-scv3-category-tree]'); 1370 trees.forEach((tree) => { 1371 Page.helpers.syncCategoryTreeExpandedState(tree); 1372 Page.helpers.updateCategoryTreeVisibility(tree); 1373 Page.helpers.updateCategoryTreeParents(tree); 1374 }); 1375 }, 1376 syncCategoryTreeExpandedState(tree) { 1377 if (!tree) return; 1378 const rows = Array.from(tree.querySelectorAll('[data-scv3-cat-row]')); 1379 rows.forEach((row) => { 1380 const termId = Number(row.getAttribute('data-scv3-cat-term-id') || 0); 1381 if (!termId) return; 1382 if (Object.prototype.hasOwnProperty.call(Page._state.categoryTreeExpanded, termId)) { 1383 const expanded = !!Page._state.categoryTreeExpanded[termId]; 1384 row.setAttribute('data-scv3-cat-expanded', expanded ? '1' : '0'); 1385 } else { 1386 Page._state.categoryTreeExpanded[termId] = row.getAttribute('data-scv3-cat-expanded') === '1'; 1387 } 1388 }); 1389 }, 1390 toggleCategoryTreeRow(row) { 1391 if (!row) return; 1392 const termId = Number(row.getAttribute('data-scv3-cat-term-id') || 0); 1393 const current = termId > 0 1394 ? !!Page._state.categoryTreeExpanded[termId] 1395 : (row.getAttribute('data-scv3-cat-expanded') === '1'); 1396 const next = !current; 1397 row.setAttribute('data-scv3-cat-expanded', next ? '1' : '0'); 1398 if (termId > 0) { 1399 Page._state.categoryTreeExpanded[termId] = next; 1400 } 1401 const tree = row.closest('[data-scv3-category-tree]') || row.parentElement; 1402 Page.helpers.updateCategoryTreeVisibility(tree); 1403 if (typeof window.requestAnimationFrame === 'function') { 1404 window.requestAnimationFrame(() => { 1405 Page.helpers.updateCategoryTreeVisibility(tree); 1406 }); 1407 } 1408 }, 1409 updateCategoryTreeVisibility(tree) { 1410 if (!tree) return; 1411 Page.helpers.syncCategoryTreeExpandedState(tree); 1412 const rows = Array.from(tree.querySelectorAll('[data-scv3-cat-row]')); 1413 if (!rows.length) return; 1414 const byTerm = new Map(); 1415 rows.forEach((row) => { 1416 const termId = Number(row.getAttribute('data-scv3-cat-term-id') || 0); 1417 if (termId) byTerm.set(termId, row); 1418 }); 1419 1420 rows.forEach((row) => { 1421 const termId = Number(row.getAttribute('data-scv3-cat-term-id') || 0); 1422 const hasChildren = row.hasAttribute('data-scv3-cat-has-children'); 1423 const expanded = termId > 0 1424 ? !!Page._state.categoryTreeExpanded[termId] 1425 : (row.getAttribute('data-scv3-cat-expanded') === '1'); 1426 row.setAttribute('data-scv3-cat-expanded', expanded ? '1' : '0'); 1427 const toggle = row.querySelector('[data-scv3-cat-toggle]'); 1428 if (toggle && hasChildren) { 1429 toggle.textContent = expanded ? '-' : '+'; 1430 toggle.setAttribute('aria-expanded', expanded ? 'true' : 'false'); 1431 } 1432 1433 let visible = true; 1434 let parentId = Number(row.getAttribute('data-scv3-cat-parent-term-id') || 0); 1435 let guard = 0; 1436 while (parentId > 0 && guard < 30) { 1437 guard += 1; 1438 const parentRow = byTerm.get(parentId); 1439 if (!parentRow) break; 1440 const parentTermId = Number(parentRow.getAttribute('data-scv3-cat-term-id') || 0); 1441 const parentExpanded = parentTermId > 0 1442 ? !!Page._state.categoryTreeExpanded[parentTermId] 1443 : (parentRow.getAttribute('data-scv3-cat-expanded') === '1'); 1444 if (!parentExpanded) { 1445 visible = false;
1446 break; 1447 } 1448 parentId = Number(parentRow.getAttribute('data-scv3-cat-parent-term-id') || 0); 1449 } 1450 row.style.display = visible ? '' : 'none'; 1451 }); 1452 }, 1453 _updateCategoryTreeParentState(row, tree) { 1454 if (!row || !tree) return; 1455 const descendantsRaw = String(row.getAttribute('data-scv3-cat-descendants') || '').trim(); 1456 if (!descendantsRaw) { 1457 const input = row.querySelector('input[type="checkbox"][data-scv3-cat-checkbox]'); 1458 if (input) { 1459 input.indeterminate = false; 1460 } 1461 return; 1462 } 1463 const ids = descendantsRaw.split(',').map((id) => Number(String(id).trim() || 0)).filter((id) => id > 0); 1464 if (!ids.length) return; 1465 const input = row.querySelector('input[type="checkbox"][data-scv3-cat-checkbox]'); 1466 if (!input) return; 1467 1468 let checkedCount = 0; 1469 ids.forEach((id) => { 1470 const childInput = tree.querySelector(`input[type="checkbox"][data-scv3-cat-checkbox][value="${id}"]`); 1471 if (childInput && childInput.checked) checkedCount += 1; 1472 }); 1473 if (checkedCount === 0) { 1474 input.indeterminate = false; 1475 return; 1476 } 1477 if (checkedCount === ids.length) { 1478 input.indeterminate = false; 1479 return; 1480 } 1481 input.indeterminate = true; 1482 }, 1483 updateCategoryTreeParents(tree) { 1484 if (!tree) return; 1485 const rows = Array.from(tree.querySelectorAll('[data-scv3-cat-row]')); 1486 for (let i = rows.length - 1; i >= 0; i -= 1) { 1487 Page.helpers._updateCategoryTreeParentState(rows[i], tree); 1488 } 1489 }, 1490 applyCategoryTreeCascade(inputEl) { 1491 if (!inputEl) return; 1492 const row = inputEl.closest('[data-scv3-cat-row]'); 1493 const tree = inputEl.closest('[data-scv3-category-tree]'); 1494 if (!row || !tree) return; 1495 1496 const descendantsRaw = String(row.getAttribute('data-scv3-cat-descendants') || '').trim(); 1497 if (descendantsRaw) { 1498 const ids = descendantsRaw.split(',').map((id) => Number(String(id).trim() || 0)).filter((id) => id > 0); 1499 const checked = !!inputEl.checked; 1500 ids.forEach((id) => { 1501 const target = tree.querySelector(`input[type="checkbox"][data-scv3-cat-checkbox][value="${id}"]`); 1502 if (!target) return; 1503 target.checked = checked; 1504 target.indeterminate = false; 1505 const targetLabel = target.closest('.scv3-filter-option'); 1506 if (targetLabel) { 1507 targetLabel.classList.toggle('scv3-is-checked', checked); 1508 } 1509 }); 1510 } 1511 1512 Page.helpers.updateCategoryTreeParents(tree); 1513 }, 1514 cloneParams(params = {}) { 1515 return JSON.parse(JSON.stringify(params || {})); 1516 }, 1517 areParamsEqual(a = {}, b = {}) { 1518 const normalize = (params) => { 1519 const normalized = {}; 1520 Object.entries(params || {}).forEach(([key, val]) => { 1521 if (Array.isArray(val)) { 1522 const items = val.map((item) => String(item)).filter((item) => item !== '').sort(); 1523 if (items.length) { 1524 normalized[key] = items; 1525 } 1526 return; 1527 } 1528 if (val !== null && val !== undefined && val !== '') { 1529 normalized[key] = [String(val)]; 1530 } 1531 }); 1532 return normalized; 1533 }; 1534 1535 const left = normalize(a); 1536 const right = normalize(b); 1537 const keysA = Object.keys(left).sort(); 1538 const keysB = Object.keys(right).sort(); 1539 if (keysA.length !== keysB.length) return false; 1540 for (let i = 0; i < keysA.length; i += 1) { 1541 const key = keysA[i]; 1542 if (key !== keysB[i]) return false; 1543 const valsA = left[key]; 1544 const valsB = right[key]; 1545 if (valsA.length !== valsB.length) return false; 1546 for (let j = 0; j < valsA.length; j += 1) { 1547 if (valsA[j] !== valsB[j]) return false; 1548 } 1549 } 1550 return true; 1551 }, 1552 isFilterEditMode() { 1553 return !!(Page._state.filterEdit && Page._state.filterEdit.active); 1554 }, 1555 getFilterEditParams() { 1556 if (Page.helpers.isFilterEditMode()) { 1557 return Page._state.filterEdit.pendingParams || {}; 1558 } 1559 return Page._state.urlParams || {}; 1560 }, 1561 setFilterEditParams(params = {}) { 1562 if (Page.helpers.isFilterEditMode()) { 1563 Page._state.filterEdit.pendingParams = params; 1564 } else { 1565 Page._state.urlParams = params; 1566 } 1567 }, 1568 beginFilterEdit() { 1569 if (Page.helpers.isFilterEditMode()) return; 1570 if (Page._state.filterTimer) { 1571 clearTimeout(Page._state.filterTimer); 1572 Page._state.filterTimer = null; 1573 } 1574 Page._state.filterEdit = { 1575 active: true,
1576 baseParams: Page.helpers.cloneParams(Page._state.urlParams || {}), 1577 pendingParams: Page.helpers.cloneParams(Page._state.urlParams || {}), 1578 page: Page._state.currentPage || 1 1579 }; 1580 }, 1581 commitFilterEdit() { 1582 const edit = Page._state.filterEdit; 1583 if (!edit || !edit.active) return; 1584 Page._state.filterEdit = null; 1585 const nextParams = edit.pendingParams || {}; 1586 if (Page.helpers.areParamsEqual(edit.baseParams, nextParams)) { 1587 return; 1588 } 1589 Page._state.urlParams = nextParams; 1590 Page._state.currentPage = 1; 1591 Page.helpers.updateUrl(1); 1592 if (typeof window.scv3UpdateMobileFilterPills === 'function') { 1593 window.scv3UpdateMobileFilterPills(); 1594 } 1595 Page.mainBlocks.runFilters(); 1596 }, 1597 cancelFilterEdit() { 1598 const edit = Page._state.filterEdit; 1599 if (!edit || !edit.active) return; 1600 Page._state.filterEdit = null; 1601 const baseParams = edit.baseParams || {}; 1602 Page._state.urlParams = baseParams; 1603 Page._state.currentPage = edit.page || Page._state.currentPage || 1; 1604 Page.helpers.updateUrl(Page._state.currentPage); 1605 Page.helpers.syncFilterInputsFromUrl(); 1606 if (typeof window.scv3UpdateMobileFilterPills === 'function') { 1607 window.scv3UpdateMobileFilterPills(); 1608 } 1609 }, 1610 escapeSelector(value) { 1611 const raw = String(value ?? ''); 1612 if (window.CSS && typeof window.CSS.escape === 'function') { 1613 return window.CSS.escape(raw); 1614 } 1615 return raw.replace(/([^\w-])/g, '\\$1'); 1616 }, 1617 captureFilterFocus() { 1618 const active = document.activeElement; 1619 if (!active || !active.closest) return null; 1620 const root = active.closest('.scv3-filters'); 1621 if (!root) return null; 1622 const tag = (active.tagName || '').toLowerCase(); 1623 if (tag !== 'input' && tag !== 'textarea') return null; 1624 1625 const state = { 1626 tag, 1627 type: active.getAttribute('type') || '', 1628 name: active.getAttribute('name') || '', 1629 slug: '', 1630 isSearch: active.hasAttribute('data-scv3-filter-search'), 1631 selectionStart: null, 1632 selectionEnd: null, 1633 scrollLeft: typeof active.scrollLeft === 'number' ? active.scrollLeft : null 1634 }; 1635 1636 const block = active.closest('.scv3-filter-block'); 1637 if (block) { 1638 state.slug = block.getAttribute('data-scv3-filter-slug') || ''; 1639 } 1640 1641 try { 1642 if (typeof active.selectionStart === 'number') { 1643 state.selectionStart = active.selectionStart; 1644 } 1645 if (typeof active.selectionEnd === 'number') { 1646 state.selectionEnd = active.selectionEnd; 1647 } 1648 } catch (e) {} 1649 1650 if (!state.name && !(state.isSearch && state.slug)) { 1651 return null; 1652 } 1653 1654 return state; 1655 }, 1656 restoreFilterFocus(state) { 1657 if (!state) return; 1658 const root = document.querySelector('.scv3-filters'); 1659 if (!root) return; 1660 const esc = Page.helpers.escapeSelector; 1661 let selector = ''; 1662 if (state.name) { 1663 selector = `input[name="${esc(state.name)}"]`; 1664 } else if (state.isSearch && state.slug) { 1665 selector = `[data-scv3-filter-slug="${esc(state.slug)}"] [data-scv3-filter-search]`; 1666 } 1667 if (!selector) return; 1668 1669 const el = root.querySelector(selector); 1670 if (!el || typeof el.focus !== 'function') return; 1671 try { 1672 el.focus({ preventScroll: true }); 1673 } catch (e) { 1674 el.focus(); 1675 } 1676 1677 if (typeof el.setSelectionRange === 'function' && 1678 state.selectionStart !== null && 1679 state.selectionEnd !== null) { 1680 const len = (el.value || '').length; 1681 const start = Math.min(state.selectionStart, len); 1682 const end = Math.min(state.selectionEnd, len); 1683 try { 1684 el.setSelectionRange(start, end); 1685 } catch (e) {} 1686 } 1687 1688 if (state.scrollLeft !== null && state.scrollLeft !== undefined) { 1689 el.scrollLeft = state.scrollLeft; 1690 } 1691 }, 1692 1693 buildFilterBlocksFromResponse(data = {}) { 1694 const blocks = []; 1695 const total = Number(data.total); 1696 const categoryBlock = data.category_block || data.categoryBlock; 1697 if (categoryBlock && Array.isArray(categoryBlock.options) && categoryBlock.options.length) { 1698 if (Number.isFinite(total) && total >= 0) { 1699 const activeOpt = categoryBlock.options.find((opt) => opt.is_active || opt.disabled); 1700 if (activeOpt) { 1701 activeOpt.count = total; 1702 } 1703 } 1704 blocks.push(categoryBlock); 1705 } 1706 1707 if (Array.isArray(data.filter_blocks)) { 1708 data.filter_blocks.forEach((block) => { 1709 if (!block || block.slug === 'categories') return; 1710 blocks.push(block); 1711 }); 1712 } else if (data.available_filters || data.availableFilters) { 1713 const available = data.available_filters || data.availableFilters || {}; 1714 const meta = data.filters_meta || data.filtersMeta || {}; 1715 const buildBlock = (slug, metaEntry = {}, availableEntry = {}) => { 1716 if (!Render || !Render.Filters || typeof Render.Filters._buildBlock !== 'function') return null; 1717 return Render.Filters._buildBlock(slug, metaEntry || {}, availableEntry || {}); 1718 }; 1719 1720 Object.entries(meta || {}
1720).forEach(([slug, metaEntry]) => { 1721 if (slug === 'categories') return; 1722 const block = buildBlock(slug, metaEntry, available[slug]); 1723 if (block) blocks.push(block); 1724 }); 1725 1726 Object.entries(available || {}).forEach(([slug, availableEntry]) => { 1727 if (slug === 'categories') return; 1728 if (blocks.find((b) => b.slug === slug)) return; 1729 const block = buildBlock(slug, {}, availableEntry); 1730 if (block) blocks.push(block); 1731 }); 1732 } 1733 1734 const priceRange = data.price_range || data.priceRange || null; 1735 const withSelection = Page.helpers.applySelectionToBlocks(blocks, priceRange); 1736 const normalizeSlug = (slug) => String(slug || '').toLowerCase(); 1737 const isPrice = (slug) => { 1738 const s = normalizeSlug(slug); 1739 return s === 'price' || s === 'cena'; 1740 }; 1741 const isCategories = (slug) => normalizeSlug(slug) === 'categories'; 1742 1743 let catBlock = null; 1744 let priceBlock = null; 1745 const rest = []; 1746 withSelection.forEach((block) => { 1747 if (!block || !block.slug) { 1748 rest.push(block); 1749 return; 1750 } 1751 if (isCategories(block.slug)) { 1752 catBlock = block; 1753 return; 1754 } 1755 if (isPrice(block.slug)) { 1756 priceBlock = block; 1757 return; 1758 } 1759 rest.push(block); 1760 }); 1761 1762 const ordered = []; 1763 if (catBlock) ordered.push(catBlock); 1764 if (priceBlock) ordered.push(priceBlock); 1765 ordered.push(...rest); 1766 return ordered; 1767 }, 1768 _collectFilterBlocks(root) { 1769 if (!root) return []; 1770 return Array.from(root.children).filter((node) => { 1771 return node && node.nodeType === 1 && node.classList.contains('scv3-filter-block'); 1772 }); 1773 }, 1774 _escapeFilterSlug(slug = '') { 1775 const value = String(slug || '').trim(); 1776 if (!value) return ''; 1777 if (typeof window !== 'undefined' && window.CSS && typeof window.CSS.escape === 'function') { 1778 return CSS.escape(value); 1779 } 1780 return value.replace(/["\\\\]/g, '\\\\$&'); 1781 }, 1782 _findNextFilterInsertionTarget(container, newBlocks, currentIndex) { 1783 if (!container || !Array.isArray(newBlocks)) return null; 1784 for (let i = currentIndex + 1; i < newBlocks.length; i += 1) { 1785 const slug = newBlocks[i]?.dataset?.scv3FilterSlug; 1786 if (!slug) continue; 1787 const selector = `[data-scv3-filter-slug="${Page.helpers._escapeFilterSlug(slug)}"]`; 1788 const nextEl = container.querySelector(selector); 1789 if (nextEl) { 1790 return nextEl; 1791 } 1792 } 1793 return null; 1794 }, 1795 _filterHtmlToElement(html = '') { 1796 const markup = String(html || '').trim(); 1797 if (!markup || typeof document === 'undefined') return null; 1798 const template = document.createElement('template'); 1799 template.innerHTML = markup; 1800 return template.content.querySelector('.scv3-filters'); 1801 }, 1802 _animateFilterBlockInsertion(block) { 1803 if (!block || !block.classList) return; 1804 block.classList.add('scv3-filter-block--animating', 'scv3-filter-block--entering'); 1805 const computed = typeof window !== 'undefined' && window.getComputedStyle 1806 ? window.getComputedStyle(block) 1807 : null; 1808 const targetMargin = computed?.marginBottom || ''; 1809 block.style.opacity = '0'; 1810 block.style.transform = 'translateY(-12px)'; 1811 block.style.maxHeight = '0px'; 1812 block.style.marginBottom = '0px'; 1813 block.getBoundingClientRect(); 1814 1815 requestAnimationFrame(() => { 1816 block.style.maxHeight = `${block.scrollHeight}px`; 1817 block.style.opacity = '1'; 1818 block.style.transform = 'translateY(0)'; 1819 if (targetMargin) { 1820 block.style.marginBottom = targetMargin; 1821 } 1822 }); 1823 1824 const cleanup = (event) => { 1825 if (event && event.propertyName !== 'max-height') return; 1826 block.removeEventListener('transitionend', cleanup); 1827 block.classList.remove('scv3-filter-block--animating', 'scv3-filter-block--entering'); 1828 block.style.maxHeight = ''; 1829 block.style.marginBottom = ''; 1830 block.style.opacity = ''; 1831 block.style.transform = ''; 1832 }; 1833 block.addEventListener('transitionend', cleanup); 1834 }, 1835 _animateFilterBlockRemoval(block) { 1836 if (!block || !block.classList || block.dataset.scv3FilterRemoving === '1') return; 1837 block.dataset.scv3FilterRemoving = '1'; 1838 block.classList.add('scv3-filter-block--animating', 'scv3-filter-block--exiting'); 1839 const computed = typeof window !== 'undefined' && window.getComputedStyle 1840 ? window.getComputedStyle(block) 1841 : null; 1842 const marginValue = computed?.marginBottom || '0px'; 1843 const height = block.getBoundingClientRect().height || block.scrollHeight; 1844 block.style.maxHeight = `${height}px`; 1845 block.style.marginBottom = marginValue; 1846 block.style.opacity = '1'; 1847 block.style.transform = 'translateY(0)'; 1848 block.getBoundingClientRect(); 1849 1850 requestAnimationFrame(() => { 1851 block.style.maxHeight = '0px'; 1852 block.style.opacity = '0'; 1853 block.style.transform = 'translateY(-12px)'; 1854 block.style.marginBottom = '0px'; 1855 }); 1856 1857 const cleanup = (event) => { 1858 if (event && event.propertyName !== 'max-height') return; 1859 block.removeEventListener('transitionend', cleanup); 1860 if (block.parentNode) { 1861 block.parentNode.removeChild(block); 1862 } 1863 }; 1864 1865 block.addEventListener('transitionend', cleanup); 1866 }, 1867 _isDesktopFilterAnimationEnabled() { 1868 if (typeof window === 'undefined' || !window.matchMedia) return false;
1869 return window.matchMedia('(min-width: 769px)').matches; 1870 }, 1871 _animateFilterBlockReplacement(existing, newBlock, reference = null) { 1872 if (!existing || !newBlock || !existing.parentNode) return false; 1873 const container = existing.parentNode; 1874 const oldHeight = existing.getBoundingClientRect().height || existing.scrollHeight || 0; 1875 const targetMargin = (typeof window !== 'undefined' && window.getComputedStyle) 1876 ? (window.getComputedStyle(newBlock).marginBottom || '') 1877 : ''; 1878 const startHeight = Math.max(1, oldHeight || newBlock.scrollHeight || 1); 1879 1880 if (reference && reference.parentNode === container) { 1881 container.insertBefore(newBlock, reference); 1882 } else { 1883 container.appendChild(newBlock); 1884 } 1885 existing.remove(); 1886 1887 newBlock.classList.add('scv3-filter-block--animating', 'scv3-filter-block--entering'); 1888 newBlock.style.maxHeight = `${startHeight}px`; 1889 newBlock.style.marginBottom = targetMargin || ''; 1890 newBlock.style.opacity = '1'; 1891 newBlock.style.transform = 'translateY(0)'; 1892 newBlock.getBoundingClientRect(); 1893 1894 let phase = 'collapse'; 1895 let timeoutId = null; 1896 const cleanup = () => { 1897 if (timeoutId) { 1898 clearTimeout(timeoutId); 1899 timeoutId = null; 1900 } 1901 newBlock.removeEventListener('transitionend', onTransitionEnd); 1902 newBlock.classList.remove('scv3-filter-block--animating', 'scv3-filter-block--entering'); 1903 newBlock.style.maxHeight = ''; 1904 newBlock.style.marginBottom = ''; 1905 newBlock.style.opacity = ''; 1906 newBlock.style.transform = ''; 1907 }; 1908 const expand = () => { 1909 phase = 'expand'; 1910 newBlock.style.maxHeight = `${Math.max(1, newBlock.scrollHeight || 1)}px`; 1911 newBlock.style.opacity = '1'; 1912 newBlock.style.transform = 'translateY(0)'; 1913 if (targetMargin) { 1914 newBlock.style.marginBottom = targetMargin; 1915 } 1916 timeoutId = setTimeout(cleanup, 420); 1917 }; 1918 const onTransitionEnd = (event) => { 1919 if (!event || event.target !== newBlock || event.propertyName !== 'max-height') return; 1920 if (phase === 'collapse') { 1921 expand(); 1922 return; 1923 } 1924 cleanup(); 1925 }; 1926 1927 newBlock.addEventListener('transitionend', onTransitionEnd); 1928 requestAnimationFrame(() => { 1929 newBlock.style.maxHeight = '0px'; 1930 newBlock.style.opacity = '0'; 1931 newBlock.style.transform = 'translateY(-12px)'; 1932 newBlock.style.marginBottom = '0px'; 1933 }); 1934 timeoutId = setTimeout(() => { 1935 if (phase === 'collapse') { 1936 expand(); 1937 return; 1938 } 1939 cleanup(); 1940 }, 420); 1941 1942 return true; 1943 }, 1944 _normalizeFilterBlockForCompare(block) { 1945 if (!block || !block.cloneNode) return ''; 1946 const clone = block.cloneNode(true); 1947 const stripClass = (el, className) => { 1948 if (el && el.classList) { 1949 el.classList.remove(className); 1950 } 1951 }; 1952 clone.removeAttribute('style'); 1953 stripClass(clone, 'scv3-filter-block--animating'); 1954 stripClass(clone, 'scv3-filter-block--entering'); 1955 stripClass(clone, 'scv3-filter-block--exiting'); 1956 stripClass(clone, 'scv3-is-open'); 1957 clone.querySelectorAll('[style]').forEach((node) => node.removeAttribute('style')); 1958 clone.querySelectorAll('[data-scv3-filter-label]').forEach((node) => node.removeAttribute('data-scv3-filter-label')); 1959 clone.querySelectorAll('.scv3-filter-list').forEach((node) => { 1960 stripClass(node, 'scv3-filter-list--scroll-active'); 1961 }); 1962 clone.querySelectorAll('.scv3-filter-more').forEach((node) => { 1963 stripClass(node, 'scv3-filter-more--expanded'); 1964 }); 1965 clone.querySelectorAll('[data-scv3-filter-option]').forEach((node) => { 1966 stripClass(node, 'scv3-filter-option--hidden'); 1967 stripClass(node, 'scv3-filter-option--search-hidden'); 1968 }); 1969 return clone.outerHTML || ''; 1970 }, 1971 _getFilterBlockUiState(block) { 1972 if (!block || !block.querySelectorAll) { 1973 return { hasMore: false, expanded: false, visibleOptionCount: 0 }; 1974 } 1975 const more = block.querySelector('[data-scv3-filter-more]'); 1976 const hasMore = !!more; 1977 const expanded = !!(more && more.classList.contains('scv3-filter-more--expanded')); 1978 const visibleOptionCount = block.querySelectorAll('[data-scv3-filter-option]:not(.scv3-filter-option--hidden
1978):not(.scv3-filter-option--search-hidden)').length; 1979 return { hasMore, expanded, visibleOptionCount }; 1980 }, 1981 _buildFilterBlockSemanticSignature(block, options = {}) { 1982 if (!block || !block.querySelectorAll) return null; 1983 const slug = String(block.dataset?.scv3FilterSlug || '').trim(); 1984 const list = block.querySelector('[data-scv3-filter-content]'); 1985 const normalizeListClass = (raw = '') => String(raw || '') 1986 .split(/\s+/) 1987 .filter(Boolean) 1988 .filter((cls) => cls !== 'scv3-filter-list--scroll-active') 1989 .sort() 1990 .join(' '); 1991 const listClass = normalizeListClass(list ? String(list.className || '') : ''); 1992 const ignoreChecked = options.ignoreChecked === true; 1993 const ignoreOrder = options.ignoreOrder === true; 1994 const compareMode = String(options.compareMode || 'all'); 1995 const firstCount = Number.isFinite(options.firstCount) && options.firstCount > 0 1996 ? options.firstCount 1997 : 0; 1998 const priceOnly = options.priceOnly === true; 1999 const allOptions = Array.from(block.querySelectorAll('[data-scv3-filter-option]')).map((opt) => { 2000 const input = opt.querySelector('input[type="checkbox"]'); 2001 return { 2002 value: input ? String(input.value || '') : '', 2003 checked: ignoreChecked ? false : !!(input && input.checked), 2004 hiddenByLimit: opt.classList.contains('scv3-filter-option--hidden'), 2005 hiddenBySearch: opt.classList.contains('scv3-filter-option--search-hidden') 2006 }; 2007 }); 2008 let optionsForCompare = allOptions; 2009 if (priceOnly) { 2010 optionsForCompare = []; 2011 } else if (firstCount > 0) { 2012 optionsForCompare = allOptions.slice(0, firstCount).map((item) => ({ 2013 value: item.value, 2014 checked: item.checked 2015 })); 2016 } else if (compareMode === 'visible_only') { 2017 optionsForCompare = allOptions 2018 .filter((item) => !item.hiddenByLimit && !item.hiddenBySearch) 2019 .map((item) => ({ 2020 value: item.value, 2021 checked: item.checked 2022 })); 2023 } else { 2024 optionsForCompare = allOptions.map((item) => ({ 2025 value: item.value, 2026 checked: item.checked 2027 })); 2028 } 2029 if (ignoreOrder) { 2030 optionsForCompare = optionsForCompare 2031 .map((item) => ({ 2032 value: item.value, 2033 checked: item.checked 2034 })) 2035 .sort((a, b) => { 2036 const av = `${a.value}|${a.checked ? '1' : '0'}`; 2037 const bv = `${b.value}|${b.checked ? '1' : '0'}`; 2038 return av.localeCompare(bv); 2039 }); 2040 } 2041 const priceInputs = Array.from(block.querySelectorAll('.scv3-filter-list--price input')).map((input) => ({ 2042 name: String(input.name || '') 2043 })); 2044 return { 2045 slug, 2046 listClass: priceOnly ? '' : listClass, 2047 priceInputs, 2048 options: optionsForCompare 2049 }; 2050 }, 2051 _toDebugJson(value) { 2052 try { 2053 return JSON.stringify(value); 2054 } catch (e) { 2055 return ''; 2056 } 2057 }, 2058 _hashDebugString(str = '') { 2059 const s = String(str || ''); 2060 let hash = 2166136261; 2061 for (let i = 0; i < s.length; i += 1) { 2062 hash ^= s.charCodeAt(i); 2063 hash += (hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24); 2064 } 2065 return (hash >>> 0).toString(16).padStart(8, '0'); 2066 }, 2067 _persistFilterAnimationDebug(entry = {}) { 2068 if (typeof window === 'undefined' || !window.localStorage) return; 2069 const enabledFlag = window.scv3FilterAnimationDebug; 2070 if (enabledFlag === false) return; 2071 const key = 'scv3-filter-debug-log'; 2072 const maxEntries = 250; 2073 let list = []; 2074 try { 2075 const raw = window.localStorage.getItem(key); 2076 const parsed = raw ? JSON.parse(raw) : []; 2077 if (Array.isArray(parsed)) list = parsed; 2078 } catch (e) { 2079 list = []; 2080 } 2081 list.push({ 2082 ts: new Date().toISOString(), 2083 ...entry 2084 }); 2085 if (list.length > maxEntries) { 2086 list = list.slice(list.length - maxEntries); 2087 } 2088 try { 2089 window.localStorage.setItem(key, JSON.stringify(list)); 2090 } catch (e) {} 2091 }, 2092 _ensureFilterDebugStorageTools() { 2093 if (typeof window === 'undefined') return; 2094 if (typeof window.scv3GetFilterAnimationDebugLog !== 'function') { 2095 window.scv3GetFilterAnimationDebugLog = () => { 2096 try { 2097 const raw = window.localStorage.getItem('scv3-filter-debug-log'); 2098 return raw ? JSON.parse(raw) : []; 2099 } catch (e) { 2100 return []; 2101 } 2102 }; 2103 } 2104 if (typeof window.scv3ClearFilterAnimationDebugLog !== 'function') { 2105 window.scv3ClearFilterAnimationDebugLog = () => { 2106 try { 2107 window.localStorage.removeItem('scv3-filter-debug-log'); 2108 } catch (e) {} 2109 return true; 2110 }; 2111 } 2112 }, 2113 _hasFilterBlockDataChanges(existing, incoming, compareOptions = {}) { 2114 const currentSemantic = Page.helpers._buildFilterBlockSemanticSignature(existing, compareOptions); 2115 const nextSemantic = Page.helpers._buildFilterBlockSemanticSignature(incoming, compareOptions); 2116 if (currentSemantic && nextSemantic) { 2117 return JSON.stringify(currentSemantic) !== JSON.stringify(nextSemantic); 2118 } 2119 const current = Page.helpers._normalizeFilterBlockForCompare(existing); 2120 const next = Page.helpers._normalizeFilterBlockForCompare(incoming); 2121 return current !== next; 2122 }, 2123 _buildSemanticCompareDebug(beforeSemantic, afterSemantic) { 2124 const toPriceNames = (semantic) => Array.isArray(semantic?.priceInputs) 2125 ? semantic.priceInputs.map((item) => String(item?.name || '')) 2126 : []; 2127 const toOptionKeys = (semantic) => Array.isArray(semantic?.options) 2128 ? semantic.options.map((item) => `${String(item?.value || '')}|${item?.checked ? '1' : '0'}`) 2129 : []; 2130 const bPrice = toPriceNames(beforeSemantic); 2131 const aPrice = toPriceNames(afterSemantic); 2132 const bOpts = toOptionKeys(beforeSemantic); 2133 const aOpts = toOptionKeys(afterSemantic); 2134 const maxLen = Math.max(bOpts.length, aOpts.length); 2135 const diffIndexes = []; 2136 for (let i = 0; i < maxLen; i += 1) { 2137 if ((bOpts[i] || '') !== (aOpts[i] || '')) { 2138 diffIndexes.push(i); 2139 if (diffIndexes.length >= 8) break; 2140 } 2141 } 2142 const samplePairs = diffIndexes.slice(0, 4).map((idx) => ({ 2143 idx, 2144 before: typeof bOpts[idx] === 'undefined' ? null : bOpts[idx], 2145 after: typeof aOpts[idx] === 'undefined' ? null : aOpts[idx] 2146 })); 2147 return { 2148 compared: { 2149 before: { 2150 listClass: String(beforeSemantic?.listClass || ''), 2151 priceInputNames: bPrice, 2152 optionsCount: bOpts.length, 2153 optionsSample: bOpts.slice(0, 12) 2154 }, 2155 after: { 2156 listClass: String(afterSemantic?.listClass || ''), 2157 priceInputNames: aPrice, 2158 optionsCount: aOpts.length, 2159 optionsSample: aOpts.slice(0, 12) 2160 } 2161 }, 2162 mismatch: { 2163 listClassChanged: String(beforeSemantic?.listClass || '') !== String(afterSemantic?.listClass || ''), 2164 priceInputNamesChanged: JSON.stringify(bPrice) !== JSON.stringify(aPrice), 2165 optionsCountChanged: bOpts.length !== aOpts.length, 2166 optionDiffIndexes: diffIndexes, 2167 optionDiffSample: samplePairs 2168 } 2169 }; 2170 }, 2171 _collectFilterBlockDebugData(block) { 2172 if (!block || !block.querySelectorAll) return null; 2173 const slug = String(block.dataset?.scv3FilterSlug || '').trim(); 2174 const labelNode = block.querySelector('.scv3-filter-block__label'); 2175 const label = (labelNode ? labelNode.textContent : '').replace(/\s+/g, ' ').trim(); 2176 const allOptions = Array.from(block.querySelectorAll('[data-scv3-filter-option]')); 2177 const options = allOptions.map((opt) => { 2178 const input = opt.querySelector('input[type="checkbox"]'); 2179 const textNode = opt.querySelector('.scv3-filter-option__text') || opt; 2180 const text = (textNode.textContent || '').replace(/\s+/g, ' ').trim(); 2181 return { 2182 label: text, 2183 value: input ? String(input.value || '') : '', 2184 checked: !!(input && input.checked), 2185 hiddenByLimit: opt.classList.contains('scv3-filter-option--hidden'), 2186 hiddenBySearch: opt.classList.contains('scv3-filter-option--search-hidden') 2187 }; 2188 }); 2189 const checkedValues = options.filter((item) => item.checked).map((item) => item.value || item.label); 2190 return { 2191 slug, 2192 label, 2193 optionsCount: options.length, 2194 checkedValues, 2195 options 2196 }; 2197 }, 2198 _logFilterAnimationDebug(payload = {}) { 2199 if (typeof window === 'undefined' || !window.console || typeof window.console.log !== 'function') return; 2200 const enabledFlag = window.scv3FilterAnimationDebug; 2201 if (enabledFlag === false) return; 2202 const verbose = window.scv3FilterAnimationDebugVerbose === true; 2203 const stamp = new Date().toISOString(); 2204 const action = payload.action || 'unknown'; 2205 const slug = payload.slug || '';
2206 const changedSuffix = Object.prototype.hasOwnProperty.call(payload, 'changed') 2207 ? ` changed=${payload.changed ? '1' : '0'}` 2208 : ''; 2209 console.log(`[scv3-filter-debug ${stamp}] action=${action} slug=${slug}${changedSuffix}`); 2210 Page.helpers._persistFilterAnimationDebug({ 2211 type: 'action', 2212 action, 2213 slug, 2214 changed: Object.prototype.hasOwnProperty.call(payload, 'changed') ? !!payload.changed : null, 2215 reason: payload.reason || '', 2216 meta: payload.meta || null 2217 }); 2218 if (!verbose) return; 2219 console.groupCollapsed(`[scv3-filter-debug-details ${stamp}] action=${action} slug=${slug}`); 2220 if (payload.reason) console.log('reason:', payload.reason); 2221 if (payload.meta) console.log('meta:', payload.meta); 2222 if (payload.before) console.log('before:', payload.before); 2223 if (payload.after) console.log('after:', payload.after); 2224 console.groupEnd(); 2225 }, 2226 _logFilterAnimationSummary(summary = {}) { 2227 if (typeof window === 'undefined' || !window.console || typeof window.console.log !== 'function') return; 2228 const enabledFlag = window.scv3FilterAnimationDebug; 2229 if (enabledFlag === false) return; 2230 const updated = Array.isArray(summary.updated) ? summary.updated : []; 2231 const skipped = Array.isArray(summary.skipped) ? summary.skipped : []; 2232 const inserted = Array.isArray(summary.inserted) ? summary.inserted : []; 2233 const removed = Array.isArray(summary.removed) ? summary.removed : []; 2234 const total = updated.length + skipped.length + inserted.length + removed.length; 2235 console.log('[scv3-filter-debug-summary]', { 2236 total, 2237 updated, 2238 skipped, 2239 inserted, 2240 removed 2241 }); 2242 Page.helpers._persistFilterAnimationDebug({ 2243 type: 'summary', 2244 total, 2245 updated, 2246 skipped, 2247 inserted, 2248 removed 2249 }); 2250 }, 2251 updateFilterBlocksWithAnimation(container, html) { 2252 const newFilters = Page.helpers._filterHtmlToElement(html); 2253 if (!newFilters || !container) return false; 2254 Page.helpers._ensureFilterDebugStorageTools(); 2255 const newBlocks = Page.helpers._collectFilterBlocks(newFilters); 2256 const existingBlocks = Page.helpers._collectFilterBlocks(container); 2257 const slugs = new Set(newBlocks.map((block) => block.dataset?.scv3FilterSlug || '')); 2258 const useReplacementAnimation = Page.helpers._isDesktopFilterAnimationEnabled(); 2259 const summary = { 2260 updated: [], 2261 skipped: [], 2262 inserted: [], 2263 removed: [] 2264 }; 2265 Page.helpers._logFilterAnimationDebug({ 2266 action: 'start', 2267 meta: { 2268 useReplacementAnimation, 2269 incomingSlugs: newBlocks.map((block) => String(block.dataset?.scv3FilterSlug || '')), 2270 existingSlugs: existingBlocks.map((block) => String(block.dataset?.scv3FilterSlug || '')) 2271 } 2272 }); 2273 2274 existingBlocks.forEach((block) => { 2275 const slug = block.dataset?.scv3FilterSlug || ''; 2276 if (slug && !slugs.has(slug)) { 2277 summary.removed.push(slug); 2278 Page.helpers._logFilterAnimationDebug({ 2279 action: 'remove', 2280 slug, 2281 reason: 'missing in backend response', 2282 before: Page.helpers._collectFilterBlockDebugData(block), 2283 after: null 2284 }); 2285 Page.helpers._animateFilterBlockRemoval(block); 2286 } 2287 }); 2288 2289 newBlocks.forEach((newBlock, idx) => { 2290 const slug = newBlock.dataset?.scv3FilterSlug || ''; 2291 const selector = slug 2292 ? `[data-scv3-filter-slug="${Page.helpers._escapeFilterSlug(slug)}"]` 2293 : null; 2294 const existing = selector ? container.querySelector(selector) : null; 2295 const reference = Page.helpers._findNextFilterInsertionTarget(container, newBlocks, idx); 2296 if (existing) { 2297 const safeReference = reference === existing ? existing.nextSibling : reference; 2298 const uiState = Page.helpers._getFilterBlockUiState(existing); 2299 const isPriceBlock = !!existing.querySelector('.scv3-filter-list--price'); 2300 const compareOptions = isPriceBlock 2301 ? { ignoreChecked: true, priceOnly: true } 2302 : ((uiState.hasMore && !uiState.expanded && uiState.visibleOptionCount > 0) 2303 ? { 2304 firstCount: uiState.visibleOptionCount, 2305 ignoreOrder: false, 2306 ignoreChecked: true 2307 } 2308 : { ignoreChecked: true }); 2309 const beforeSemantic = Page.helpers._buildFilterBlockSemanticSignature(existing, compareOptions); 2310 const afterSemantic = Page.helpers._buildFilterBlockSemanticSignature(newBlock, compareOptions); 2311 const beforeJson = Page.helpers._toDebugJson(beforeSemantic); 2312 const afterJson = Page.helpers._toDebugJson(afterSemantic); 2313 const hasChanges = beforeJson !== afterJson; 2314 const semanticCompare = Page.helpers._buildSemanticCompareDebug(beforeSemantic, afterSemantic); 2315 const signatureMeta = { 2316 beforeHash: Page.helpers._hashDebugString(beforeJson), 2317 afterHash: Page.helpers._hashDebugString(afterJson), 2318 beforeSize: beforeJson.length, 2319 afterSize: afterJson.length 2320 }; 2321 if (!hasChanges) { 2322 // Keep block intact to avoid unnecessary re-render and animation. 2323 summary.skipped.push(slug); 2324 Page.helpers._logFilterAnimationDebug({ 2325 action: 'skip', 2326 slug, 2327 changed: false, 2328 reason: 'block data is identical', 2329 meta: { compareOptions, uiState, signature: signatureMeta, compare: semanticCompare }, 2330 before: Page.helpers._collectFilterBlockDebugData(existing), 2331 after: Page.helpers._collectFilterBlockDebugData(newBlock) 2332 }); 2333 if (safeReference) { 2334 if (safeReference !== existing) { 2335 container.insertBefore(existing, safeReference); 2336 } 2337 } else { 2338 container.appendChild(existing); 2339 } 2340 return; 2341 } 2342 summary.updated.push(slug); 2343 Page.helpers._logFilterAnimationDebug({ 2344 action: useReplacementAnimation ? 'replace-animated' : 'replace', 2345 slug, 2346 changed: true, 2347 meta: { compareOptions, uiState, signature: signatureMeta, compare: semanticCompare }, 2348 before: Page.helpers._collectFilterBlockDebugData(existing), 2349 after: Page.helpers._collectFilterBlockDebugData(newBlock) 2350 }); 2351 if (useReplacementAnimation) { 2352 Page.helpers._animateFilterBlockReplacement(existing, newBlock, safeReference); 2353 } else { 2354 existing.replaceWith(newBlock); 2355 // Ensure correct ordering even if the block already existed. 2356 if (safeReference) { 2357 if (safeReference !== newBlock) { 2358 container.insertBefore(newBlock, safeReference); 2359 } 2360 } else { 2361 container.appendChild(newBlock); 2362 } 2363 } 2364 } else { 2365 summary.inserted.push(slug); 2366 Page.helpers._logFilterAnimationDebug({ 2367 action: 'insert', 2368 slug, 2369 changed: true, 2370 reason: 'new block from backend response', 2371 before: null, 2372 after: Page.helpers._collectFilterBlockDebugData(newBlock) 2373 }); 2374 container.insertBefore(newBlock, reference); 2375 Page.helpers._animateFilterBlockInsertion(newBlock); 2376 } 2377 }); 2378 Page.helpers._logFilterAnimationSummary(summary); 2379 2380 return true; 2381 }, 2382 buildHeaderData(data = {}) { 2383 const baseTitle = 'ÐÑе ÑоваÑÑ ÐºÐ°Ñалога slamdunk'; 2384 let title = baseTitle; 2385 const breadcrumbs = [ 2386 { label: 'ÐлавнаÑ', url: '/' }, 2387 { label: baseTitle, url: '/shop/' } 2388 ]; 2389
2390 const params = Page._state.urlParams || {}; 2391 const searchText = typeof params.s === 'string' ? params.s.trim() : ''; 2392 const route = data.route || data.routeInfo || {}; 2393 const routeType = String(route.route_type || route.type || '').toLowerCase(); 2394 const extra = data.route_extra || data.routeExtra || {}; 2395 const termsPath = Array.isArray(extra.terms_path) ? extra.terms_path : []; 2396 2397 let skipCategories = false; 2398 if (searchText) { 2399 title = `РезÑлÑÑÐ°Ñ Ð¿Ð¾Ð¸Ñка: â${searchText}â`; 2400 breadcrumbs.push({ label: title }); 2401 skipCategories = true; 2402 } else if (routeType === 'tags' && termsPath.length) { 2403 const tag = termsPath[termsPath.length - 1]; 2404 const tagName = Page.helpers.stripLegacySeoTitlePlaceholder(tag?.name || tag?.slug || ''); 2405 if (tagName) { 2406 title = tagName; 2407 breadcrumbs.push({ label: title }); 2408 skipCategories = true; 2409 } 2410 } 2411 2412 if (routeType === 'cat' && termsPath.length) { 2413 const segments = []; 2414 termsPath.forEach((term, idx) => { 2415 const slug = String(term?.slug || '').trim(); 2416 if (!slug) return; 2417 segments.push(slug); 2418 const name = Page.helpers.stripLegacySeoTitlePlaceholder(term?.name || slug); 2419 breadcrumbs.push({ 2420 label: name, 2421 url: idx === termsPath.length - 1 ? '' : `/cat/${segments.join('/')}/` 2422 }); 2423 }); 2424 if (segments.length) { 2425 title = breadcrumbs[breadcrumbs.length - 1].label; 2426 skipCategories = true; 2427 } 2428 } 2429 2430 if (!skipCategories) { 2431 const categories = Array.isArray(data.categories) 2432 ? data.categories 2433 : (Array.isArray(Page._state.categories) ? Page._state.categories : []); 2434 const activeSlug = String( 2435 data.active_category_slug || data.activeCategorySlug || Page._state.activeCategorySlug || '' 2436 ).toLowerCase().trim(); 2437 2438 if (activeSlug && categories.length) { 2439 const byId = new Map(); 2440 const bySlug = new Map(); 2441 categories.forEach((item) => { 2442 const id = Number(item.term_id || item.id || item.termId || 0); 2443 const slug = String(item.slug || '').toLowerCase().trim(); 2444 if (!id || !slug) return; 2445 const parent = Number(item.parent || item.parent_id || item.parentId || 0); 2446 const name = Page.helpers.stripLegacySeoTitlePlaceholder(item.name || item.title || slug); 2447 const entry = { id, slug, parent, name }; 2448 byId.set(id, entry); 2449 bySlug.set(slug, entry); 2450 }); 2451 2452 const chain = [];
2453 let current = bySlug.get(activeSlug) || null; 2454 let guard = 0; 2455 while (current && guard < 12) { 2456 chain.unshift(current); 2457 const parentId = current.parent || 0; 2458 current = parentId ? byId.get(parentId) : null; 2459 guard += 1; 2460 } 2461 2462 if (chain.length) { 2463 const segments = []; 2464 chain.forEach((term, idx) => { 2465 segments.push(term.slug); 2466 const url = `/cat/${segments.join('/')}/`; 2467 breadcrumbs.push({ 2468 label: term.name, 2469 url: idx === chain.length - 1 ? '' : url 2470 }); 2471 }); 2472 title = chain[chain.length - 1].name; 2473 } 2474 } 2475 } 2476 2477 const page = Page._state.currentPage || 1; 2478 if (page > 1) { 2479 title = `${title} - СÑÑаниÑа ${page}`; 2480 const lastIdx = breadcrumbs.length - 1; 2481 if (lastIdx >= 0) { 2482 breadcrumbs[lastIdx].label = title; 2483 } 2484 } 2485 if (data.page_meta && data.page_meta.h1) { 2486 title = Page.helpers.stripLegacySeoTitlePlaceholder(data.page_meta.h1); 2487 if (page > 1) { 2488 title += ` - СÑÑаниÑа ${page}`; 2489 } 2490 } 2491 2492 return { title, breadcrumbs }; 2493 }, 2494 buildMetaData(data = {}) { 2495 const params = Page._state.urlParams || {}; 2496 const page = Page._state.currentPage || 1; 2497 const navigation = Page.helpers.resolveLandingNavigation(params); 2498 const hasQuery = Object.keys(navigation.query || {}).length > 0; 2499 const headerData = Page.helpers.buildHeaderData(data); 2500 const route = data.route || data.routeInfo || {}; 2501 const routeType = String(route.route_type || route.type || '').toLowerCase(); 2502 const extra = data.route_extra || data.routeExtra || {}; 2503 const termsPath = Array.isArray(extra.terms_path) ? extra.terms_path : []; 2504 2505 let metaTitle = 'ÐÑе ÑоваÑÑ Ð² каÑалоге ÑеÑвиÑа Slamdunk.shop'; 2506 let metaDescription = null; 2507 2508 const searchText = typeof params.s === 'string' ? params.s.trim() : ''; 2509 if (searchText) { 2510 if (page > 1) { 2511 metaTitle = `РезÑлÑÑÐ°Ñ Ð¿Ð¾Ð¸Ñка â${searchText}â â СÑÑаниÑа ${page}`; 2512 } else { 2513 metaTitle = `${searchText} â Slamdunk.Shop`; 2514 } 2515 metaDescription = ''; 2516 } else if (routeType === 'cat' || routeType === 'tags') { 2517 let termName = ''; 2518 if (termsPath.length) { 2519 const last = termsPath[termsPath.length - 1]; 2520 termName = Page.helpers.stripLegacySeoTitlePlaceholder(last?.name || last?.slug || ''); 2521 } 2522 if (!termName) { 2523 termName = Page.helpers.stripLegacySeoTitlePlaceholder(headerData.title || ''); 2524 } 2525 if (termName) { 2526 if (page > 1) { 2527 metaTitle = `${termName} - СÑÑаниÑа ${page}`; 2528 metaDescription = `${termName} - СÑÑаниÑа ${page}.`; 2529 } else { 2530 metaTitle = `${termName} кÑпиÑÑ Ð¾Ñигинал - Ñена в ÐоÑкве и РоÑÑии Ñ Ð±ÐµÑплаÑной доÑÑавкой`; 2531 metaDescription = `Цена ${termName} - оÑигиналÑнÑе ÑоваÑÑ ÑпоÑÑивнÑÑ Ð±Ñендов в инÑеÑнеÑ-магазине СлÑмданк.`; 2532 } 2533 } 2534 } 2535 2536 if (navigation.landing && data.page_meta) { 2537 metaTitle = String(data.page_meta.title || metaTitle); 2538 metaDescription = data.page_meta.description ?? metaDescription; 2539 } 2540 const canonical = Render.Helpers.pageUrl(Render.Helpers.buildPageUrl(navigation.path, 1, {})); 2541 const nextPage = page < Page._state.totalPages ? page + 1 : null; 2542 const prevPage = page > 1 ? page - 1 : null; 2543 const nextUrl = nextPage 2544 ? Render.Helpers.pageUrl(Render.Helpers.buildPageUrl(navigation.path, nextPage, navigation.query)) 2545 : ''; 2546 const prevUrl = prevPage 2547 ? Render.Helpers.pageUrl(Render.Helpers.buildPageUrl(navigation.path, prevPage, navigation.query)) 2548 : ''; 2549 const robots = hasQuery 2550 ? 'noindex, follow' 2551 : 'index, follow, max-image-preview:large, max-snippet:-1, max-video-preview:-1'; 2552 const modifiedRaw = String( 2553 data.page_last_modified_gmt 2554 || data.pageLastModifiedGmt 2555 || '' 2556 ).trim(); 2557 const existingModified = document.head.querySelector('meta[property="article:modified_time"]')?.getAttribute('content') || ''; 2558 let articleModifiedTime = modifiedRaw || String(existingModified || '').trim(); 2559 if (articleModifiedTime && !articleModifiedTime.includes('T')) { 2560 const dt = new Date(articleModifiedTime); 2561 if (!Number.isNaN(dt.getTime())) { 2562 articleModifiedTime = dt.toISOString(); 2563 } 2564 } 2565 2566 return { 2567 title: metaTitle, 2568 description: metaDescription, 2569 og_title: metaTitle, 2570 og_description: metaDescription, 2571 canonical, 2572 og_url: canonical, 2573 next: nextUrl, 2574 prev: prevUrl, 2575 robots, 2576 article_modified_time: articleModifiedTime || undefined 2577 }; 2578 }, 2579 updateHeader(data = {}, total = null) { 2580 if (!Render || !Render.Header) return; 2581 const title = data.title || '';
2582 const breadcrumbs = Array.isArray(data.breadcrumbs) ? data.breadcrumbs : []; 2583 const crumbsHtml = Render.Header.renderBreadcrumbs(breadcrumbs); 2584 let crumbsReplaced = false; 2585 if (crumbsHtml) { 2586 const $crumbs = $('.crumbs.crumbs__container').first(); 2587 if ($crumbs.length) { 2588 $crumbs.replaceWith(crumbsHtml); 2589 crumbsReplaced = true; 2590 } 2591 } 2592 if (crumbsReplaced && typeof window.bc_renew === 'function') { 2593 window.bc_renew(); 2594 } 2595 const headerHtml = Render.Header.renderHeader({ title, count: total }); 2596 if (headerHtml) { 2597 const $header = $('.header-page').first(); 2598 if ($header.length) { 2599 $header.replaceWith(headerHtml); 2600 } 2601 } 2602 }, 2603 updateMeta(meta = {}) { 2604 if (!Render || !Render.Meta) return; 2605 Render.Meta.apply(meta); 2606 }, 2607 updateMainTextSpacing(totalPages = null) { 2608 const pages = Number.isFinite(Number(totalPages)) 2609 ? Number(totalPages) 2610 : Number(Page._state.totalPages || 1); 2611 const $mainText = $('.main-text').first(); 2612 if (!$mainText.length) return; 2613 $mainText.toggleClass('main-text--no-pagination', pages < 2); 2614 }, 2615 updateTermDescription(descriptionHtml = '') { 2616 const $mainText = $('.main-text').first(); 2617 if (!$mainText.length) return; 2618 const $desc = $mainText.find('.term-description').first(); 2619 if (!$desc.length) return; 2620 const html = String(descriptionHtml || '').trim(); 2621 if (html === '') { 2622 $desc.html(''); 2623 $mainText.hide(); 2624 return; 2625 } 2626 $desc.html(html); 2627 $mainText.show(); 2628 }, 2629 updateTermAdminInfo(data = {}) { 2630 const block = document.querySelector(Page._selectors.termAdminInfo); 2631 if (!block) return; 2632 2633 const idNode = block.querySelector('[data-scv3-term-admin-id]'); 2634 const linkNode = block.querySelector('[data-scv3-term-admin-link]'); 2635 if (!idNode || !linkNode) return; 2636 2637 let termId = 0; 2638 let routeType = ''; 2639 let href = ''; 2640 2641 const apiInfo = (data && typeof data.catalog_term_admin_info === 'object' && data.catalog_term_admin_info) 2642 ? data.catalog_term_admin_info 2643 : ((data && typeof data.catalogTermAdminInfo === 'object' && data.catalogTermAdminInfo) ? data.catalogTermAdminInfo : null); 2644 if (apiInfo) { 2645 termId = Number(apiInfo.term_id || apiInfo.termId || 0); 2646 href = String(apiInfo.edit_url || apiInfo.editUrl || '').trim(); 2647 } 2648 2649 if (!Number.isFinite(termId) || termId <= 0) { 2650 const route = (data && typeof data.route === 'object' && data.route) ? data.route : {}; 2651 routeType = String(route.route_type || route.routeType || '').trim(); 2652 const routeExtra = (data && typeof data.route_extra === 'object' && data.route_extra) 2653 ? data.route_extra 2654 : ((data && typeof data.routeExtra === 'object' && data.routeExtra) ? data.routeExtra : {}); 2655 const termsPathRaw = routeExtra.terms_path || routeExtra.termsPath || []; 2656 const termsPath = Array.isArray(termsPathRaw) ? termsPathRaw : []; 2657 if ((routeType === 'cat' || routeType === 'tags') && termsPath.length) { 2658 const last = termsPath[termsPath.length - 1]; 2659 if (last && typeof last === 'object') { 2660 termId = Number(last.term_id || last.termId || 0); 2661 } 2662 } 2663 if (Number.isFinite(termId) && termId > 0) { 2664 const taxonomy = routeType === 'cat' ? 'product_cat' : 'product_tag'; 2665 href = `/wp-admin/term.php?taxonomy=${encodeURIComponent(taxonomy)}&tag_ID=${Math.trunc(termId)}&post_type=product`; 2666 } 2667 } 2668 2669 if (!Number.isFinite(termId) || termId <= 0 || !href) { 2670 block.style.display = 'none'; 2671 return; 2672 } 2673 2674 idNode.textContent = String(Math.trunc(termId)); 2675 linkNode.setAttribute('href', href); 2676 block.style.display = ''; 2677 }, 2678 setListNavigationLoading(isLoading = false) { 2679 const on = !!isLoading; 2680 document.documentElement.classList.toggle('scv3-list-loading', on); 2681 if (document.body) { 2682 document.body.classList.toggle('scv3-list-loading', on); 2683 } 2684 }, 2685 ensurePagination() { 2686 if (!Render || !Render.Pagination) return; 2687 const totalPages = Page._state.totalPages || 1; 2688 Page.helpers.updateMainTextSpacing(totalPages); 2689 const $existing = $(Page._selectors.pagination).first(); 2690 if (totalPages < 2) { 2691 if ($existing.length) { 2692 $existing.remove(); 2693 } 2694 return; 2695 } 2696 2697 const currentPage = Page._state.uiPage || Page._state.currentPage || 1; 2698 const navigation = Page.helpers.resolveLandingNavigation(); 2699 const paginationHtml = Render.Pagination.render({ 2700 totalPages, 2701 currentPage, 2702 baseUrl: navigation.path, 2703 queryParams: navigation.query 2704 }); 2705 if (!paginationHtml) return; 2706 if ($existing.length) { 2707 $existing.replaceWith(paginationHtml); 2708 Page.dynamicLisaners.pagination(); 2709 return; 2710 } 2711 2712 const $loadMoreWrap = $('.scv3-load-more-wrap').first(); 2713 if ($loadMoreWrap.length) { 2714 $loadMoreWrap.after(paginationHtml); 2715 } else { 2716 const $grid = $(Page._selectors.cards.productsBlock).first(); 2717 if ($grid.length) { 2718 $grid.after(paginationHtml); 2719 } 2720 } 2721 Page.dynamicLisaners.pagination(); 2722 }, 2723 applySelectionToBlocks(blocks = [], priceRange = null) {
2724 const params = Page._state.urlParams || {}; 2725 const selectedMap = {}; 2726 Object.entries(params).forEach(([k, v]) => { 2727 if (Array.isArray(v)) { 2728 selectedMap[k] = v.map((x) => String(x)); 2729 } else if (v !== undefined && v !== null) { 2730 selectedMap[k] = [String(v)]; 2731 } 2732 }); 2733 2734 const priceMeta = Page.helpers.normalizePriceRange(priceRange); 2735 blocks.forEach((block) => { 2736 if (!block || !block.slug) return; 2737 if (block.type === 'price') { 2738 const fromName = block.from_name || 'pa-price_from'; 2739 const toName = block.to_name || 'pa-price_to'; 2740 const hasFrom = selectedMap[fromName] && selectedMap[fromName][0] !== undefined; 2741 const hasTo = selectedMap[toName] && selectedMap[toName][0] !== undefined; 2742 const selectedFrom = hasFrom ? selectedMap[fromName][0] : priceMeta.selected_from; 2743 const selectedTo = hasTo ? selectedMap[toName][0] : priceMeta.selected_to; 2744 const phFrom = Page.helpers.formatPriceDisplay(priceMeta.min); 2745 const phTo = Page.helpers.formatPriceDisplay(priceMeta.max); 2746 const displayFrom = Page.helpers.formatPriceDisplay(selectedFrom); 2747 const displayTo = Page.helpers.formatPriceDisplay(selectedTo); 2748 2749 block.value_from = displayFrom || (selectedFrom !== null && selectedFrom !== undefined ? String(selectedFrom) : ''); 2750 block.value_to = displayTo || (selectedTo !== null && selectedTo !== undefined ? String(selectedTo) : ''); 2751 block.placeholder_from = phFrom || block.placeholder_from || 'ÐÑ'; 2752 block.placeholder_to = phTo || block.placeholder_to || 'Ðо'; 2753 return; 2754 } 2755 2756 const opts = Array.isArray(block.options) ? block.options : []; 2757 if (block.type === 'category_tree') { 2758 opts.forEach((opt) => { 2759 const termId = Number(opt.term_id || 0); 2760 if (!termId) return; 2761 if (Object.prototype.hasOwnProperty.call(Page._state.categoryTreeExpanded, termId)) { 2762 opt.expanded = !!Page._state.categoryTreeExpanded[termId]; 2763 } 2764 }); 2765 } 2766 opts.forEach((opt) => { 2767 const nameAttr = 2768 opt.name_attr || 2769 opt.name || 2770 block.input_name || 2771 block.name || 2772 `pa-${block.slug}[]`; 2773 const normalizedNameAttr = Page.helpers.normalizeFilterParamName(nameAttr); 2774 const values = selectedMap[normalizedNameAttr] || selectedMap[nameAttr] || []; 2775 const val = opt.value !== undefined ? String(opt.value) : ''; 2776 const isSelected = val !== '' && values.includes(val); 2777 if (isSelected) { 2778 opt.checked = true; 2779 opt.is_active = true; 2780 } 2781 }); 2782 block.options = opts; 2783 }); 2784 2785 return blocks; 2786 }, 2787 serializeUrlParams(params) { 2788 const usp = new URLSearchParams(); 2789 Object.entries(Page.helpers.normalizeUrlParams(params)).forEach(([k, v]) => { 2790 if (Array.isArray(v)) { 2791 v.forEach((item) => { 2792 if (item !== null && item !== undefined && item !== '') usp.append(k, item); 2793 }); 2794 } else if (v !== null && v !== undefined && v !== '') { 2795 usp.append(k, v); 2796 } 2797 }); 2798 return usp; 2799 }, 2800 normalizeFilterParamName(name) { 2801 const value = String(name || ''); 2802 return /^pa-[a-z0-9_-]+\[\]$/i.test(value) ? value.slice(0, -2) : value; 2803 }, 2804 stripLegacySeoTitlePlaceholder(value) { 2805 return String(value || '') 2806 .replaceAll('{SEO_TITLE}', '') 2807 .replace(/\s{2,}/g, ' ') 2808 .replace(/\s+([,.;:!?])/g, '$1') 2809 .trim(); 2810 }, 2811 normalizeUrlParams(params = {}) { 2812 const normalized = {}; 2813 Object.entries(params || {}).forEach(([rawKey, rawValue]) => { 2814 const key = Page.helpers.normalizeFilterParamName(rawKey); 2815 const values = Array.isArray(rawValue) ? rawValue : [rawValue]; 2816 values.forEach((value) => { 2817 if (value === null || value === undefined || value === '') return; 2818 if (normalized[key] === undefined) { 2819 normalized[key] = value; 2820 } else if (Array.isArray(normalized[key])) { 2821 if (!normalized[key].map(String).includes(String(value))) { 2822 normalized[key].push(value); 2823 } 2824 } else if (String(normalized[key]) !== String(value)) { 2825 normalized[key] = [normalized[key], value]; 2826 } 2827 }); 2828 }); 2829 return normalized; 2830 }, 2831 normalizeLandingContext(context) { 2832 if (!context || typeof context !== 'object') return null; 2833 const pages = Array.isArray(context.pages) ? context.pages : []; 2834 return { 2835 basePath: String(context.base_path || context.basePath || '').replace(/^\/+|\/+$/g, ''), 2836 pages, 2837 active: context.active && typeof context.active === 'object' ? context.active : null, 2838 selectedAttributes: context.selected_attributes && typeof context.selected_attributes === 'object' 2839 ? context.selected_attributes 2840 : (context.selectedAttributes && typeof context.selectedAttributes === 'object' 2841 ? context.selectedAttributes 2842 : {}) 2843 }; 2844 }, 2845 landingAttributesKey(attributes = {}) { 2846 return Object.keys(attributes) 2847 .sort() 2848 .map((slug) => { 2849 const values = (Array.isArray(attributes[slug]) ? attributes[slug] : [attributes[slug]]) 2850 .map((value) => Number(value)) 2851 .filter((value) => Number.isFinite(value) && value > 0) 2852 .sort((a, b) => a - b); 2853 return values.length ? `${slug}=${values.join(',')}` : ''; 2854 }) 2855 .filter(Boolean) 2856 .join('&'); 2857 },
2858 resolveLandingNavigation(params = Page._state.urlParams) { 2859 const context = Page._state.landingContext; 2860 const basePath = context?.basePath || Page._state.basePath || ''; 2861 if (!context || !Array.isArray(context.pages) || !context.pages.length) { 2862 return { path: basePath, query: { ...(params || {}) }, landing: null }; 2863 } 2864 2865 const attributes = {}; 2866 const query = { ...(params || {}) }; 2867 Object.entries(params || {}).forEach(([rawKey, rawValue]) => { 2868 const key = String(rawKey || '').replace(/\[\]$/, ''); 2869 const match = key.match(/^pa-([a-z0-9_-]+)$/i); 2870 if (!match) { 2871 return; 2872 } 2873 const values = (Array.isArray(rawValue) ? rawValue : [rawValue]) 2874 .flatMap((value) => String(value).split(',')) 2875 .map((value) => Number(value)) 2876 .filter((value) => Number.isFinite(value) && value > 0); 2877 if (!values.length) { 2878 return; 2879 } 2880 attributes[match[1]] = values; 2881 }); 2882 2883 const landing = context.pages 2884 .slice() 2885 .sort((a, b) => Object.keys(b?.attributes || {}).length - Object.keys(a?.attributes || {}).length) 2886 .find((page) => { 2887 const pageAttributes = page?.attributes || {}; 2888 return Object.entries(pageAttributes).every(([slug, values]) => { 2889 const selected = (Array.isArray(attributes[slug]) ? attributes[slug] : [attributes[slug]]) 2890 .map((value) => Number(value)) 2891 .filter((value) => Number.isFinite(value) && value > 0) 2892 .sort((a, b) => a - b); 2893 const expected = (Array.isArray(values) ? values : [values]) 2894 .map((value) => Number(value)) 2895 .filter((value) => Number.isFinite(value) && value > 0) 2896 .sort((a, b) => a - b); 2897 return expected.length > 0 2898 && expected.length === selected.length 2899 && expected.every((value, idx) => value === selected[idx]); 2900 }); 2901 }); 2902 if (!landing) { 2903 return { path: basePath, query, landing: null }; 2904 } 2905 Object.keys(landing.attributes || {}).forEach((slug) => { 2906 delete query[`pa-${slug}`]; 2907 delete query[`pa-${slug}[]`]; 2908 }); 2909 return { 2910 path: `${basePath}/${String(landing.url || '').replace(/^\/+|\/+$/g, '')}`, 2911 query, 2912 landing 2913 }; 2914 }, 2915 getRouteState() { 2916 const rawPath = (location.pathname || '').replace(/^\/+|\/+$/g, ''); 2917 const matchPage = rawPath.match(/^(.*?)(?:\/page\/(\d+))?$/); 2918 let basePath = (matchPage && matchPage[1]) ? matchPage[1].replace(/\/+$/,'') : rawPath; 2919 const currentPage = matchPage && matchPage[2] ? Math.max(1, parseInt(matchPage[2], 10)) : 1; 2920 2921 // /cat/ без Ñлага â ÑÑо Ð³Ð»Ð°Ð²Ð½Ð°Ñ ÑÑÑаниÑа (оÑдаÑм вÑе ÑоваÑÑ) 2922 if (!basePath || /^cat\/?$/.test(basePath)) { 2923 basePath = ''; 2924 } 2925 2926 const urlParams = {}; 2927 const usp = new URLSearchParams(location.search || '');
2928 usp.forEach((v, rawKey) => { 2929 if (v === null || v === undefined || v === '') return; 2930 const k = Page.helpers.normalizeFilterParamName(rawKey); 2931 const isCompactFilter = /^pa-[a-z0-9_-]+(?:\[\])?$/i.test(String(k || '')); 2932 if (isCompactFilter && String(v).includes(',')) { 2933 const items = String(v) 2934 .split(',') 2935 .map((item) => item.trim()) 2936 .filter((item) => item !== ''); 2937 if (!items.length) return; 2938 if (urlParams[k] !== undefined) { 2939 if (Array.isArray(urlParams[k])) { 2940 urlParams[k].push(...items); 2941 } else { 2942 urlParams[k] = [urlParams[k], ...items]; 2943 } 2944 } else { 2945 urlParams[k] = items; 2946 } 2947 return; 2948 } 2949 if (urlParams[k] !== undefined) { 2950 if (Array.isArray(urlParams[k])) { 2951 urlParams[k].push(v); 2952 } else { 2953 urlParams[k] = [urlParams[k], v]; 2954 } 2955 } else { 2956 urlParams[k] = v; 2957 } 2958 }); 2959 const initialLanding = Page.helpers.normalizeLandingContext( 2960 Page._state?.landingContext || window.scv3LandingContext || null 2961 ); 2962 const pathWithoutPage = basePath; 2963 const matchedPage = initialLanding?.pages?.find((page) => { 2964 const landingPath = `${initialLanding.basePath}/${String(page?.url || '').replace(/^\/+|\/+$/g, '')}`; 2965 return landingPath === pathWithoutPage; 2966 }); 2967 if (matchedPage) { 2968 Object.entries(matchedPage.attributes || {}).forEach(([slug, values]) => { 2969 if (urlParams[`pa-${slug}`] !== undefined || urlParams[`pa-${slug}[]`] !== undefined) { 2970 return; 2971 } 2972 const normalized = Array.isArray(values) ? values.map(String) : [String(values)]; 2973 urlParams[`pa-${slug}`] = normalized.length === 1 ? normalized[0] : normalized; 2974 }); 2975 basePath = initialLanding.basePath || basePath; 2976 } 2977 2978 return { 2979 currentPage, 2980 basePath, 2981 urlParams, 2982 perPage: 24, 2983 total: 0, 2984 totalPages: 1 2985 }; 2986 }, 2987 fetchAndRender({ page = 1, append = false, withFacets = true, history = 'replace', updateUrl = true, updatePagination = true } = {}) { 2988 const navigation = Page.helpers.resolveLandingNavigation(); 2989 const pathForRequest = navigation.path 2990 ? `${navigation.path}${page > 1 ? '/page/' + page : ''}` 2991 : (page > 1 ? `page/${page}` : ''); 2992 Page.helpers.setListNavigationLoading(true); 2993 2994 const $grid = $(Page._selectors.cards.productsBlock); 2995 if ($grid.length) { 2996 if (append) { 2997 if (!$grid.find('[data-scv3-loader]').length) { 2998 $grid.append(Render.Loading.spinner()); 2999 } 3000 } else { 3001 $grid.html(Render.Loading.spinner()); 3002 } 3003 } 3004 3005 Back.getProducts({ 3006 path: pathForRequest, 3007 with_facets: withFacets ? 1 : 0, 3008 ...navigation.query 3009 }).then(res => { 3010 if (res.isErr) { 3011 if ($grid.length) { 3012 $grid.find('[data-scv3-loader]').remove(); 3013 } 3014 alert(res.err); 3015 return; 3016 } 3017 3018 const data = res.data || {}; 3019 if (data.landing_context) { 3020 Page._state.landingContext = Page.helpers.normalizeLandingContext(data.landing_context); 3021 } 3022 Page.helpers.cacheProducts(data.items || [], { reset: !append }); 3023 const filterBlocks = Page.helpers.buildFilterBlocksFromResponse(data); 3024 if (filterBlocks.length) { 3025 const focusState = Page.helpers.captureFilterFocus(); 3026 const filtersHtml = Render.Filters.render(filterBlocks); 3027 if (filtersHtml) { 3028 let $filters = $(Page._selectors.filters.categories).first().closest('.scv3-filters'); 3029 if (!$filters.length) { 3030 $filters = $('.scv3-filters').first(); 3031 } 3032 const filtersElement = $filters.length ? $filters.get(0) : null; 3033 const animatedUpdate = filtersElement 3034 ? Page.helpers.updateFilterBlocksWithAnimation(filtersElement, filtersHtml) 3035 : false; 3036 if (!animatedUpdate && $filters.length) { 3037 $filters.replaceWith(filtersHtml); 3038 } 3039 Page.helpers.syncFilterInputsFromUrl(); 3040 scv3InitFilterBlocks(); 3041 scv3InitFilterColors(); 3042 Page.helpers.initCategoryTrees(); 3043 Page.dynamicLisaners.categories(); 3044 Page.dynamicLisaners.categoriesTree(); 3045 Page.dynamicLisaners.options(); 3046 Page.dynamicLisaners.price(); 3047 if (typeof window.scv3UpdateMobileFilterPills === 'function') { 3048 window.scv3UpdateMobileFilterPills(); 3049 } 3050 Page.helpers.restoreFilterFocus(focusState); 3051 } 3052 } 3053 3054 if (Render?.Helpers?.isProductUrlDebugEnabled && Render.Helpers.isProductUrlDebugEnabled()) { 3055 try { 3056 console.warn('[scv3][product-url] api response items', { 3057 page, 3058 append, 3059 basePath: Page._state.basePath,
3060 urlParams: Page._state.urlParams, 3061 items: data.items || [] 3062 }); 3063 } catch (e) {} 3064 } 3065 3066 const productsHtml = Render.Cards.cards(data.items || []); 3067 if ($grid.length) { 3068 $grid.find('[data-scv3-loader]').remove(); 3069 if (append) { 3070 $grid.append(productsHtml); 3071 } else { 3072 $grid.html(productsHtml); 3073 } 3074 Page.dynamicLisaners.cardVariants(); 3075 Page.dynamicLisaners.pagination(); 3076 Page.dynamicLisaners.categories(); 3077 Page.dynamicLisaners.categoriesTree(); 3078 Page.dynamicLisaners.options(); 3079 Page.dynamicLisaners.price(); 3080 Page.dynamicLisaners.sorting(); 3081 if (typeof window.scv3InitProductSliders === 'function') { 3082 window.scv3InitProductSliders(); 3083 } 3084 } 3085 3086 if (data) { 3087 if (Page.helpers.isNumeric(data.per_page)) { 3088 Page._state.perPage = Number(data.per_page); 3089 } 3090 if (Page.helpers.isNumeric(data.total)) { 3091 Page._state.total = Number(data.total); 3092 } 3093 if (Array.isArray(data.categories)) { 3094 Page._state.categories = data.categories; 3095 } 3096 const activeSlug = data.active_category_slug || data.activeCategorySlug; 3097 if (activeSlug !== undefined && activeSlug !== null) { 3098 Page._state.activeCategorySlug = String(activeSlug || ''); 3099 } 3100 Page._state.totalPages = Math.max(1, Math.ceil(Page._state.total / Page._state.perPage)); 3101 Page.helpers.setHeaderCount(Page._state.total); 3102 } 3103 3104 Page._state.currentPage = page; 3105 if (!append) { 3106 Page._state.uiPage = page; 3107 } 3108 if (!append) { 3109 Page.helpers.updateHeader(Page.helpers.buildHeaderData(data), Page._state.total); 3110 Page.helpers.updateMeta(Page.helpers.buildMetaData(data)); 3111 } 3112 if (updateUrl) { 3113 Page.helpers.updateUrl(page, history); 3114 } 3115 Page.helpers.updateTermDescription(data.term_description_html || ''); 3116 Page.helpers.updateTermAdminInfo(data); 3117 3118 const itemsCount = Array.isArray(data.items) ? data.items.length : 0; 3119 const shouldShowLoadMore = Page._state.totalPages > page && itemsCount >= Page._state.perPage; 3120 const nextLoadMoreUrl = shouldShowLoadMore 3121 ? Render.Helpers.buildPageUrl( 3122 Page.helpers.resolveLandingNavigation().path, 3123 page + 1, 3124 Page.helpers.resolveLandingNavigation().query 3125 ) 3126 : ''; 3127 let $loadMoreWrap = $('.scv3-load-more-wrap').first(); 3128 if (shouldShowLoadMore) { 3129 if (!$loadMoreWrap.length) { 3130 const $grid = $(Page._selectors.cards.productsBlock).first(); 3131 if ($grid.length) { 3132 $loadMoreWrap = $(` 3133 <div class="scv3-load-more-wrap"> 3134 <a class="scv3-button scv3-load-more" href="#">ÐоказаÑÑ ÐµÑе</a> 3135 </div> 3136 `); 3137 $grid.after($loadMoreWrap); 3138 Page.staticLisaners(); 3139 } 3140 } 3141 if ($loadMoreWrap.length) { 3142 const $btn = $loadMoreWrap.find(Page._selectors.loadMoreBtn).first(); 3143 if ($btn.length && nextLoadMoreUrl) { 3144 $btn.attr('href', nextLoadMoreUrl); 3145 } 3146 $loadMoreWrap.show(); 3147 } 3148 } else if ($loadMoreWrap.length) { 3149 $loadMoreWrap.hide(); 3150 } 3151 3152 if (updatePagination) { 3153 const paginationHtml = Render.Pagination.render({ 3154 totalPages: Page._state.totalPages, 3155 currentPage: page, 3156 baseUrl: Page.helpers.resolveLandingNavigation().path, 3157 queryParams: Page.helpers.resolveLandingNavigation().query 3158 }); 3159 if (paginationHtml) { 3160 const $pagination = $(Page._selectors.pagination).first(); 3161 if ($pagination.length) { 3162 $pagination.replaceWith(paginationHtml); 3163 Page.dynamicLisaners.pagination(); 3164 } else { 3165 let $loadMoreWrap = $('.scv3-load-more-wrap').first(); 3166 if ($loadMoreWrap.length) { 3167 $loadMoreWrap.after(paginationHtml); 3168 } else { 3169 const $grid = $(Page._selectors.cards.productsBlock).first(); 3170 if ($grid.length) { 3171 $grid.after(paginationHtml); 3172 } 3173 } 3174 Page.dynamicLisaners.pagination(); 3175 } 3176 } else { 3177 const $pagination = $(Page._selectors.pagination).first(); 3178 if ($pagination.length) { 3179 $pagination.remove(); 3180 } 3181 } 3182 } else { 3183 Page.helpers.ensurePagination(); 3184 } 3185 Page.helpers.updateMainTextSpacing(Page._state.totalPages); 3186 }).finally(() => { 3187 Page.helpers.setListNavigationLoading(false); 3188 }); 3189 }, 3190 updateUrl(page, mode = 'replace') { 3191 const navigation = Page.helpers.resolveLandingNavigation(); 3192 const newUrl = Render.Helpers.buildPageUrl( 3193 navigation.path, 3194 page, 3195 navigation.query 3196 ); 3197 const currentUrl = location.pathname + location.search; 3198 if (newUrl === currentUrl) { 3199 return; 3200 } 3201 if (typeof history !== 'undefined') { 3202 if (mode === 'push' && typeof history.pushState === 'function') { 3203 history.pushState({ scv3: true }, '', newUrl); 3204 } else if (typeof history.replaceState === 'function') { 3205 history.replaceState({ scv3: true }, '', newUrl); 3206 } 3207 } 3208 }, 3209 setHeaderCount(count) { 3210 let el = $(Page._selectors.header_p_count); 3211 3212 if(el.length === 0) 3213 return; 3214 3215 const text = (Render && Render.Helpers && Render.Helpers.productsCountText) 3216 ? Render.Helpers.productsCountText(count) 3217 : `${count} ÑоваÑов`; 3218 $(el[0]).text(text); 3219 }, 3220 closeMobileFilters() { 3221 const mq = window.matchMedia ? window.matchMedia('(max-width: 768px)') : null; 3222 if (mq && !mq.matches) return; 3223 if (typeof window.scv3CloseFilterAllSheet === 'function') { 3224 window.scv3CloseFilterAllSheet(); 3225 } 3226 if (typeof window.scv3CloseFilterSheet === 'function') { 3227 window.scv3CloseFilterSheet(); 3228 } 3229 }, 3230 updateSliderVisibility() { 3231 /* 3232 const slider = document.querySelector('.scv3-category-slider'); 3233 if (!slider || typeof Page === 'undefined') return; 3234 const basePath = Page?._state?.basePath || '';
3235 const hasSearch = !!(Page?._state?.urlParams?.s); 3236 const isCatalog = !hasSearch && (!basePath || basePath === 'shop' || basePath.indexOf('cat/') === 0); 3237 slider.style.display = isCatalog ? '' : 'none'; 3238 */ 3239 }, 3240 isNumeric(str) { 3241 if (str === null || str === undefined) return false; 3242 if (typeof str === "number") return Number.isFinite(str); 3243 if (typeof str !== "string") return false; 3244 const trimmed = str.trim(); 3245 if (!trimmed) return false; 3246 return !isNaN(trimmed) && isFinite(Number(trimmed)); 3247 } 3248 }, 3249}; 3250 3251Page.preload(); 3252 3253(function () { 3254 const sheet = document.querySelector('[data-scv3-sort-sheet]'); 3255 const triggers = document.querySelectorAll('[data-scv3-sort-trigger]'); 3256 if (!sheet || !triggers.length || typeof Page === 'undefined') return; 3257 3258 const closeEls = sheet.querySelectorAll('[data-scv3-sort-close]'); 3259 const applyBtn = sheet.querySelector('[data-scv3-sort-apply]'); 3260 const inputs = Array.from(sheet.querySelectorAll('input[name="scv3-sort-mobile"]')); 3261 if (!inputs.length) return; 3262 3263 let currentSort = ''; 3264 3265 const normalizeSort = (val) => { 3266 if (!val) return ''; 3267 const v = String(val).trim(); 3268 if (v === 'popular') return 'popularity'; 3269 return v; 3270 }; 3271 3272 const getCurrentSort = () => { 3273 const stateSort = normalizeSort(Page?._state?.urlParams?.sort || ''); 3274 if (stateSort) return stateSort; 3275 const active = document.querySelector('.scv3-sort-dropdown__option.scv3-is-active'); 3276 if (active) { 3277 const dataSort = active.getAttribute('data-sort') || active.getAttribute('data-sort-key') || ''; 3278 return normalizeSort(dataSort || active.textContent || ''); 3279 } 3280 return 'popularity'; 3281 }; 3282 3283 const setState = (state) => { 3284 sheet.setAttribute('data-scv3-sort-state', state); 3285 }; 3286 3287 const syncInputs = (value) => { 3288 inputs.forEach((input) => { 3289 input.checked = input.value === value; 3290 }); 3291 }; 3292 3293 const openSheet = () => { 3294 currentSort = getCurrentSort(); 3295 syncInputs(currentSort); 3296 setState('idle'); 3297 sheet.classList.add('scv3-is-open'); 3298 sheet.setAttribute('aria-hidden', 'false'); 3299 document.documentElement.classList.add('scv3-sort-lock'); 3300 document.body.classList.add('scv3-sort-lock'); 3301 }; 3302 3303 const closeSheet = (restore = true) => { 3304 sheet.classList.remove('scv3-is-open'); 3305 sheet.setAttribute('aria-hidden', 'true'); 3306 document.documentElement.classList.remove('scv3-sort-lock'); 3307 document.body.classList.remove('scv3-sort-lock'); 3308 if (restore) { 3309 currentSort = getCurrentSort(); 3310 syncInputs(currentSort); 3311 setState('idle'); 3312 } 3313 }; 3314 3315 const getSelected = () => { 3316 const checked = inputs.find((input) => input.checked); 3317 return checked ? normalizeSort(checked.value) : ''; 3318 }; 3319 3320 triggers.forEach((btn) => { 3321 btn.addEventListener('click', (e) => { 3322 e.preventDefault(); 3323 openSheet(); 3324 }); 3325 }); 3326 3327 closeEls.forEach((el) => { 3328 el.addEventListener('click', (e) => { 3329 e.preventDefault(); 3330 closeSheet(true); 3331 }); 3332 }); 3333 3334 inputs.forEach((input) => { 3335 input.addEventListener('change', () => { 3336 const selected = getSelected(); 3337 setState(selected && selected !== currentSort ? 'changed' : 'idle'); 3338 }); 3339 }); 3340
3341 if (applyBtn) { 3342 applyBtn.addEventListener('click', (e) => { 3343 e.preventDefault(); 3344 const selected = getSelected(); 3345 if (!selected || selected === currentSort) { 3346 closeSheet(true); 3347 return; 3348 } 3349 Page._state.urlParams = { ...Page._state.urlParams, sort: selected }; 3350 Page._state.currentPage = 1; 3351 Page.helpers.updateUrl(1); 3352 Page.helpers.fetchAndRender({ 3353 page: 1, 3354 append: false, 3355 withFacets: false 3356 }); 3357 Page.helpers.closeMobileFilters(); 3358 currentSort = selected; 3359 closeSheet(false); 3360 }); 3361 } 3362 3363 document.addEventListener('keydown', (e) => { 3364 if (e.key === 'Escape' && sheet.classList.contains('scv3-is-open')) { 3365 closeSheet(true); 3366 } 3367 }); 3368})(); 3369 3370(function () { 3371 const sheet = document.querySelector('[data-scv3-filter-sheet]'); 3372 const triggers = document.querySelectorAll('[data-scv3-filter-trigger]'); 3373 const body = sheet ? sheet.querySelector('[data-scv3-filter-body]') : null; 3374 if (!sheet || !body || !triggers.length || typeof Page === 'undefined') return; 3375 3376 const closeEls = sheet.querySelectorAll('[data-scv3-filter-close]'); 3377 const resetBtn = sheet.querySelector('[data-scv3-filter-reset]'); 3378 const applyBtn = sheet.querySelector('[data-scv3-filter-apply]'); 3379 let anchorParent = null; 3380 let anchorNext = null; 3381 let editSessionActive = false; 3382 3383 const escSelector = (value) => { 3384 if (window.CSS && typeof window.CSS.escape === 'function') { 3385 return window.CSS.escape(value); 3386 } 3387 return String(value || '').replace(/"/g, '\\"'); 3388 }; 3389 3390 const getFilters = () => document.querySelector('.scv3-filters'); 3391 3392 const clearInputs = (filters) => { 3393 if (!filters) return; 3394 filters.querySelectorAll('.scv3-filter-option input[type="checkbox"], .scv3-filter-option input[type="radio"]').forEach((input) => { 3395 input.checked = false; 3396 input.indeterminate = false; 3397 const label = input.closest('.scv3-filter-option'); 3398 if (label) { 3399 label.classList.remove('scv3-is-checked'); 3400 } 3401 }); 3402 filters.querySelectorAll('.scv3-filter-list--price input').forEach((input) => { 3403 input.value = ''; 3404 }); 3405 }; 3406 3407 const ensureCategoryMobileTrigger = (filters) => { 3408 if (!filters) return; 3409 const block = filters.querySelector('[data-scv3-filter-slug="categories"]'); 3410 if (!block) return; 3411 3412 const toggle = block.querySelector('[data-scv3-filter-toggle]'); 3413 if (toggle) { 3414 toggle.setAttribute('data-scv3-filter-mobile-open', '1'); 3415 } 3416 3417 let more = block.querySelector('[data-scv3-filter-more]'); 3418 if (!more) { 3419 more = document.createElement('button'); 3420 more.type = 'button'; 3421 more.className = 'scv3-filter-more'; 3422 more.setAttribute('data-scv3-filter-more', ''); 3423 more.setAttribute('data-label-expand', 'ÐÑе'); 3424 more.setAttribute('data-label-collapse', 'ÐÑе'); 3425 more.innerHTML = '<span data-scv3-filter-more-label>ÐÑе</span>'; 3426 block.appendChild(more); 3427 } 3428 more.setAttribute('data-scv3-filter-mobile-open', '1'); 3429 block.classList.add('scv3-filter-block--mobile-link'); 3430 3431 const content = block.querySelector('[data-scv3-filter-content]'); 3432 if (content) { 3433 content.style.display = 'none'; 3434 } 3435 block.classList.remove('scv3-is-open'); 3436 }; 3437 3438 const openSheet = () => { 3439 const filters = getFilters(); 3440 if (!filters) return; 3441 if (!anchorParent) { 3442 anchorParent = filters.parentElement; 3443 anchorNext = filters.nextSibling; 3444 } 3445 body.appendChild(filters); 3446 if (typeof scv3InitFilterBlocks === 'function') { 3447 scv3InitFilterBlocks(); 3448 } 3449 if (typeof scv3InitFilterColors === 'function') { 3450 scv3InitFilterColors(); 3451 } 3452 ensureCategoryMobileTrigger(filters); 3453 if (window.matchMedia('(max-width: 768px)').matches && Page.helpers && typeof Page.helpers.beginFilterEdit === 'function') { 3454 Page.helpers.beginFilterEdit(); 3455 editSessionActive = true; 3456 } else { 3457 editSessionActive = false; 3458 } 3459 sheet.classList.add('scv3-is-open'); 3460 sheet.setAttribute('aria-hidden', 'false'); 3461 document.documentElement.classList.add('scv3-filter-lock'); 3462 document.body.classList.add('scv3-filter-lock'); 3463 }; 3464 3465 const closeSheet = (action = 'cancel') => { 3466 const filters = getFilters(); 3467 if (editSessionActive && Page.helpers && typeof Page.helpers.isFilterEditMode === 'function') { 3468 if (Page.helpers.isFilterEditMode()) { 3469 if (action === 'apply' && typeof Page.helpers.commitFilterEdit === 'function') { 3470 Page.helpers.commitFilterEdit(); 3471 } else if (typeof Page.helpers.cancelFilterEdit === 'function') { 3472 Page.helpers.cancelFilterEdit(); 3473 } 3474 } 3475 editSessionActive = false;
3476 } 3477 if (filters && anchorParent) { 3478 if (anchorNext && anchorNext.parentNode === anchorParent) { 3479 anchorParent.insertBefore(filters, anchorNext); 3480 } else { 3481 anchorParent.appendChild(filters); 3482 } 3483 } 3484 sheet.classList.remove('scv3-is-open'); 3485 sheet.setAttribute('aria-hidden', 'true'); 3486 document.documentElement.classList.remove('scv3-filter-lock'); 3487 document.body.classList.remove('scv3-filter-lock'); 3488 }; 3489 3490 const scrollToSlug = (slug) => { 3491 if (!slug) return; 3492 const target = body.querySelector(`[data-scv3-filter-slug="${escSelector(slug)}"]`); 3493 if (!target) return; 3494 const top = Math.max(0, target.offsetTop - 12); 3495 body.scrollTo({ top, behavior: 'smooth' }); 3496 }; 3497 3498 const resetFilters = () => { 3499 const filters = getFilters(); 3500 if (!filters) return; 3501 clearInputs(filters); 3502 const paramsSource = Page.helpers.isFilterEditMode && Page.helpers.isFilterEditMode() 3503 ? Page.helpers.getFilterEditParams() 3504 : Page._state.urlParams; 3505 const params = { ...(paramsSource || {}) }; 3506 const names = new Set(); 3507 filters.querySelectorAll('input[name]').forEach((input) => { 3508 let name = input.getAttribute('name') || ''; 3509 if (!name) return; 3510 if (name.endsWith('[]')) { 3511 name = name.slice(0, -2); 3512 } 3513 names.add(name); 3514 }); 3515 names.forEach((name) => { 3516 delete params[name]; 3517 delete params[`${name}[]`]; 3518 }); 3519 if (Page.helpers.isFilterEditMode && Page.helpers.isFilterEditMode()) { 3520 Page.helpers.setFilterEditParams(params); 3521 if (typeof window.scv3UpdateMobileFilterPills === 'function') { 3522 window.scv3UpdateMobileFilterPills(); 3523 } 3524 return; 3525 } 3526 Page._state.urlParams = params; 3527 Page._state.currentPage = 1; 3528 Page.helpers.updateUrl(1); 3529 Page.mainBlocks.runFilters(); 3530 }; 3531 3532 window.scv3OpenFilterSheet = (slug = '') => { 3533 openSheet(); 3534 if (slug) { 3535 setTimeout(() => scrollToSlug(slug), 50); 3536 } 3537 }; 3538 3539 window.scv3CloseFilterSheet = () => { 3540 closeSheet(); 3541 }; 3542 3543 triggers.forEach((btn) => { 3544 btn.addEventListener('click', (e) => { 3545 e.preventDefault(); 3546 openSheet(); 3547 }); 3548 }); 3549 3550 closeEls.forEach((el) => { 3551 el.addEventListener('click', (e) => { 3552 e.preventDefault(); 3553 closeSheet(); 3554 }); 3555 }); 3556 3557 if (resetBtn) { 3558 resetBtn.addEventListener('click', (e) => { 3559 e.preventDefault(); 3560 resetFilters(); 3561 }); 3562 } 3563 3564 if (applyBtn) { 3565 applyBtn.addEventListener('click', (e) => { 3566 e.preventDefault(); 3567 closeSheet('apply'); 3568 }); 3569 } 3570 3571 body.addEventListener('click', (e) => { 3572 const trigger = e.target.closest('[data-scv3-filter-mobile-open]'); 3573 if (!trigger) return; 3574 const block = trigger.closest('.scv3-filter-block'); 3575 if (!block) return; 3576 e.preventDefault(); 3577 e.stopPropagation(); 3578 if (typeof window.scv3OpenFilterAllSheet === 'function') { 3579 window.scv3OpenFilterAllSheet('', { block, fromFilters: true }); 3580 } 3581 }); 3582 3583 document.addEventListener('keydown', (e) => { 3584 if (e.key === 'Escape' && sheet.classList.contains('scv3-is-open')) { 3585 closeSheet('cancel'); 3586 } 3587 }); 3588})(); 3589 3590(function () { 3591 const sheet = document.querySelector('[data-scv3-filter-all-sheet]'); 3592 const body = sheet ? sheet.querySelector('[data-scv3-filter-all-body]') : null; 3593 const titleEl = sheet ? sheet.querySelector('[data-scv3-filter-all-title]') : null; 3594 if (!sheet || !body || typeof Page === 'undefined') return; 3595 3596 const filterSheet = document.querySelector('[data-scv3-filter-sheet]'); 3597 const closeEls = sheet.querySelectorAll('[data-scv3-filter-all-close]'); 3598 const backBtn = sheet.querySelector('[data-scv3-filter-all-back]'); 3599 const applyBtn = sheet.querySelector('[data-scv3-filter-all-apply]'); 3600 let currentBlock = null; 3601 let anchorParent = null; 3602 let anchorNext = null; 3603 let prevOpen = false; 3604 let prevDisplay = ''; 3605 let expandedByScript = false; 3606 let moreEl = null; 3607 let ignoreMoreClick = false; 3608 let editSessionActive = false; 3609 3610 const isMobile = () => window.matchMedia('(max-width: 768px)').matches; 3611 3612 const escSelector = (value) => { 3613 if (window.CSS && typeof window.CSS.escape === 'function') { 3614 return window.CSS.escape(value); 3615 } 3616 return String(value || '').replace(/"/g, '\\"'); 3617 }; 3618 3619 const getLabelText = (el) => { 3620 if (!el) return ''; 3621 const clone = el.cloneNode(true); 3622 clone.querySelectorAll('.scv3-filter-count').forEach((node) => node.remove()); 3623 return (clone.textContent || '').replace(/\s+/g, ' ').trim(); 3624 }; 3625 3626 const getBlockBySlug = (slug) => { 3627 if (!slug) return null; 3628 return document.querySelector(`[data-scv3-filter-slug="${escSelector(slug)}"]`); 3629 }; 3630 3631 const ensureExpanded = (block) => { 3632 expandedByScript = false;
3633 moreEl = block.querySelector('[data-scv3-filter-more]'); 3634 if (!moreEl) return; 3635 if (!moreEl.classList.contains('scv3-filter-more--expanded')) { 3636 expandedByScript = true; 3637 ignoreMoreClick = true; 3638 moreEl.click(); 3639 } 3640 }; 3641 3642 const openSheet = (block, fromFilters) => { 3643 if (!block) return; 3644 currentBlock = block; 3645 anchorParent = block.parentElement; 3646 anchorNext = block.nextSibling; 3647 prevOpen = block.classList.contains('scv3-is-open'); 3648 const content = block.querySelector('[data-scv3-filter-content]'); 3649 prevDisplay = content ? content.style.display : ''; 3650 3651 ensureExpanded(block); 3652 3653 const label = getLabelText(block.querySelector('.scv3-filter-block__label')); 3654 if (titleEl) { 3655 titleEl.textContent = label || 'ФилÑÑÑ'; 3656 } 3657 3658 body.appendChild(block); 3659 block.classList.add('scv3-is-open'); 3660 if (content) { 3661 content.style.display = 'grid'; 3662 } 3663 3664 if (isMobile() && Page.helpers && typeof Page.helpers.beginFilterEdit === 'function') { 3665 Page.helpers.beginFilterEdit(); 3666 editSessionActive = true; 3667 } else { 3668 editSessionActive = false; 3669 } 3670 3671 sheet.classList.toggle('scv3-filter-all-sheet--full', !!fromFilters); 3672 sheet.classList.add('scv3-is-open'); 3673 sheet.setAttribute('aria-hidden', 'false'); 3674 document.documentElement.classList.add('scv3-filter-all-lock'); 3675 document.body.classList.add('scv3-filter-all-lock'); 3676 3677 const searchInput = body.querySelector('[data-scv3-filter-search]'); 3678 if (searchInput) { 3679 searchInput.focus(); 3680 } 3681 }; 3682 3683 const closeSheet = (action = 'cancel') => { 3684 if (currentBlock && anchorParent) { 3685 const content = currentBlock.querySelector('[data-scv3-filter-content]'); 3686 if (content) { 3687 content.style.display = prevDisplay || ''; 3688 } 3689 if (!prevOpen) { 3690 currentBlock.classList.remove('scv3-is-open'); 3691 }
3692 if (anchorNext && anchorNext.parentNode === anchorParent) { 3693 anchorParent.insertBefore(currentBlock, anchorNext); 3694 } else { 3695 anchorParent.appendChild(currentBlock); 3696 } 3697 } 3698 3699 if (moreEl && expandedByScript) { 3700 ignoreMoreClick = true; 3701 moreEl.click(); 3702 } 3703 3704 if (editSessionActive && Page.helpers && typeof Page.helpers.isFilterEditMode === 'function') { 3705 if (Page.helpers.isFilterEditMode()) { 3706 if (action === 'apply' && typeof Page.helpers.commitFilterEdit === 'function') { 3707 Page.helpers.commitFilterEdit(); 3708 } else if (typeof Page.helpers.cancelFilterEdit === 'function') { 3709 Page.helpers.cancelFilterEdit(); 3710 } 3711 } 3712 editSessionActive = false; 3713 } 3714 3715 currentBlock = null; 3716 anchorParent = null; 3717 anchorNext = null; 3718 prevOpen = false; 3719 prevDisplay = ''; 3720 expandedByScript = false; 3721 moreEl = null; 3722 3723 sheet.classList.remove('scv3-is-open', 'scv3-filter-all-sheet--full'); 3724 sheet.setAttribute('aria-hidden', 'true'); 3725 document.documentElement.classList.remove('scv3-filter-all-lock'); 3726 document.body.classList.remove('scv3-filter-all-lock'); 3727 }; 3728 3729 window.scv3OpenFilterAllSheet = (slug = '', options = {}) => { 3730 const block = options.block || getBlockBySlug(slug); 3731 if (!block) return; 3732 const fromFilters = typeof options.fromFilters === 'boolean' 3733 ? options.fromFilters 3734 : !!(filterSheet && filterSheet.classList.contains('scv3-is-open') && block.closest('[data-scv3-filter-sheet]')); 3735 openSheet(block, fromFilters); 3736 }; 3737 3738 window.scv3CloseFilterAllSheet = () => { 3739 closeSheet(); 3740 }; 3741 3742 document.addEventListener('click', (e) => { 3743 const more = e.target.closest('[data-scv3-filter-more]'); 3744 if (!more) return; 3745 if (!isMobile()) return; 3746 if (ignoreMoreClick) { 3747 ignoreMoreClick = false; 3748 return; 3749 } 3750 const block = more.closest('.scv3-filter-block'); 3751 if (!block) return; 3752 const fromFilters = !!(filterSheet && filterSheet.classList.contains('scv3-is-open')) || 3753 !!more.closest('[data-scv3-filter-sheet]'); 3754 openSheet(block, fromFilters); 3755 }); 3756 3757 closeEls.forEach((el) => { 3758 el.addEventListener('click', (e) => { 3759 e.preventDefault(); 3760 if (sheet.classList.contains('scv3-filter-all-sheet--full') && 3761 el.classList.contains('scv3-filter-all-sheet__overlay')) { 3762 return; 3763 } 3764 closeSheet('cancel'); 3765 }); 3766 }); 3767 3768 if (backBtn) { 3769 backBtn.addEventListener('click', (e) => { 3770 e.preventDefault(); 3771 closeSheet('apply'); 3772 }); 3773 } 3774 3775 if (applyBtn) { 3776 applyBtn.addEventListener('click', (e) => { 3777 e.preventDefault(); 3778 closeSheet('apply'); 3779 }); 3780 } 3781 3782 document.addEventListener('keydown', (e) => { 3783 if (e.key === 'Escape' && sheet.classList.contains('scv3-is-open')) { 3784 closeSheet('cancel'); 3785 } 3786 }); 3787})(); 3788 3789(function () { 3790 const roots = Array.from(document.querySelectorAll('[data-scv3-filter-pills]')); 3791 if (!roots.length) return; 3792 3793 const escSelector = (value) => { 3794 if (window.CSS && typeof window.CSS.escape === 'function') { 3795 return window.CSS.escape(value); 3796 } 3797 return String(value || '').replace(/"/g, '\\"'); 3798 }; 3799 3800 const getFilters = () => document.querySelector('.scv3-filters'); 3801 3802 const getLabelText = (el) => { 3803 if (!el) return ''; 3804 const clone = el.cloneNode(true); 3805 clone.querySelectorAll('.scv3-filter-count').forEach((node) => node.remove()); 3806 return (clone.textContent || '').replace(/\s+/g, ' ').trim(); 3807 }; 3808 3809 const buildBlockPills = (filters, blocksContainer) => { 3810 if (!blocksContainer) return; 3811 blocksContainer.innerHTML = ''; 3812 const blocks = Array.from(filters.querySelectorAll('.scv3-filter-block')); 3813 blocks.forEach((block) => { 3814 const slug = (block.dataset.scv3FilterSlug || '').trim(); 3815 const isPriceBlock = slug === 'price' || slug === 'cena'; 3816 let label = getLabelText(block.querySelector('.scv3-filter-block__label')); 3817 if (isPriceBlock) { 3818 label = 'Цена'; 3819 } 3820 if (!label) return; 3821 const btn = document.createElement('button'); 3822 btn.type = 'button'; 3823 btn.className = 'scv3-filter-pill scv3-filter-pill--block'; 3824 btn.textContent = label; 3825 if (slug) { 3826 btn.dataset.scv3FilterTarget = slug; 3827 } 3828 blocksContainer.appendChild(btn); 3829 }); 3830 }; 3831 3832 const buildActivePills = (filters, activeContainer) => { 3833 if (!activeContainer) return 0; 3834 activeContainer.innerHTML = ''; 3835 let count = 0; 3836 3837 const appendActive = (label, data = {}) => { 3838 const btn = document.createElement('button');
3839 btn.type = 'button'; 3840 btn.className = 'scv3-filter-pill scv3-filter-pill--active'; 3841 btn.dataset.scv3FilterRemove = '1'; 3842 Object.entries(data).forEach(([key, val]) => { 3843 btn.dataset[key] = val; 3844 }); 3845 btn.appendChild(document.createTextNode(label)); 3846 const close = document.createElement('span'); 3847 close.className = 'scv3-filter-pill__close scv3-filter-icon material-symbols-rounded'; 3848 close.setAttribute('aria-hidden', 'true'); 3849 close.textContent = 'close'; 3850 btn.appendChild(close); 3851 activeContainer.appendChild(btn); 3852 count += 1; 3853 }; 3854 3855 const appendCategoryTreeGroups = () => { 3856 const tree = filters.querySelector('[data-scv3-filter-slug="categories"] [data-scv3-category-tree]') || 3857 filters.querySelector('[data-scv3-category-tree]'); 3858 if (!tree) return; 3859 const rows = Array.from(tree.querySelectorAll('[data-scv3-cat-row]')); 3860 if (!rows.length) return; 3861 const byTerm = new Map(); 3862 rows.forEach((row) => { 3863 const termId = Number(row.getAttribute('data-scv3-cat-term-id') || 0); 3864 if (termId > 0) { 3865 byTerm.set(termId, row); 3866 } 3867 }); 3868 const groups = new Map(); 3869 rows.forEach((row) => { 3870 const input = row.querySelector('input[type="checkbox"][data-scv3-cat-checkbox]'); 3871 if (!input || !input.checked) return; 3872 let topRow = row; 3873 let parentId = Number(row.getAttribute('data-scv3-cat-parent-term-id') || 0); 3874 let guard = 0; 3875 while (parentId > 0 && guard < 30) { 3876 guard += 1; 3877 const parentRow = byTerm.get(parentId); 3878 if (!parentRow) break; 3879 topRow = parentRow; 3880 parentId = Number(parentRow.getAttribute('data-scv3-cat-parent-term-id') || 0); 3881 } 3882 const topTermId = Number(topRow.getAttribute('data-scv3-cat-term-id') || 0); 3883 if (!topTermId) return; 3884 const topLabel = getLabelText(topRow.querySelector('.scv3-category-tree__label')) || 'ÐаÑегоÑиÑ'; 3885 const topInput = topRow.querySelector('input[type="checkbox"][data-scv3-cat-checkbox]'); 3886 const name = (topInput && topInput.getAttribute('name')) || input.getAttribute('name') || ''; 3887 const value = String(input.value || '').trim(); 3888 if (!value || !name) return; 3889 if (!groups.has(topTermId)) { 3890 groups.set(topTermId, { label: topLabel, name, values: new Set(), subCount: 0 }); 3891 } 3892 const group = groups.get(topTermId); 3893 group.values.add(value); 3894 const rowTermId = Number(row.getAttribute('data-scv3-cat-term-id') || 0); 3895 if (rowTermId !== topTermId) { 3896 group.subCount += 1; 3897 } 3898 }); 3899 3900 groups.forEach((group) => { 3901 const selectedCount = group.values.size; 3902 if (selectedCount <= 0) return; 3903 let label = group.label; 3904 if (group.subCount > 0) { 3905 label = `${group.label} (${group.subCount})`; 3906 } 3907 appendActive(label, { 3908 scv3FilterType: 'categoryGroup', 3909 scv3FilterName: group.name, 3910 scv3FilterValues: Array.from(group.values).join(',') 3911 }); 3912 }); 3913 }; 3914 3915 appendCategoryTreeGroups(); 3916 3917 const checkedInputs = Array.from( 3918 filters.querySelectorAll('.scv3-filter-option input[type="checkbox"]:checked') 3919 ); 3920 checkedInputs.forEach((input) => { 3921 if (input.hasAttribute('data-scv3-cat-checkbox')) return; 3922 const label = getLabelText(input.closest('.scv3-filter-option')); 3923 if (!label) return; 3924 appendActive(label, { 3925 scv3FilterName: input.getAttribute('name') || '', 3926 scv3FilterValue: input.value || '' 3927 }); 3928 }); 3929 3930 const priceInputs = Array.from(filters.querySelectorAll('.scv3-filter-list--price input')); 3931 let priceFrom = ''; 3932 let priceTo = ''; 3933 priceInputs.forEach((input) => { 3934 const name = input.getAttribute('name') || '';
3935 const value = String(input.value || '').trim(); 3936 if (!value) return; 3937 if (/_from$/.test(name)) { 3938 priceFrom = value; 3939 } else if (/_to$/.test(name)) { 3940 priceTo = value; 3941 } 3942 }); 3943 if (priceFrom || priceTo) { 3944 let label = 'Цена'; 3945 if (priceFrom && priceTo) { 3946 label = `Цена ${priceFrom}â${priceTo}`; 3947 } else if (priceFrom) { 3948 label = `Цена Ð¾Ñ ${priceFrom}`; 3949 } else if (priceTo) { 3950 label = `Цена до ${priceTo}`; 3951 } 3952 appendActive(label, { scv3FilterType: 'price' }); 3953 } 3954 3955 return count; 3956 }; 3957 3958 const resetAllFilters = () => { 3959 const filters = getFilters(); 3960 if (!filters) return; 3961 const params = { ...Page._state.urlParams }; 3962 const names = new Set(); 3963 filters.querySelectorAll('input[name]').forEach((input) => { 3964 let name = input.getAttribute('name') || ''; 3965 if (!name) return; 3966 if (name.endsWith('[]')) { 3967 name = name.slice(0, -2); 3968 } 3969 names.add(name); 3970 }); 3971 names.forEach((name) => { 3972 delete params[name]; 3973 delete params[`${name}[]`]; 3974 }); 3975 filters.querySelectorAll('.scv3-filter-option input[type="checkbox"], .scv3-filter-option input[type="radio"]').forEach((input) => { 3976 input.checked = false; 3977 input.indeterminate = false; 3978 const label = input.closest('.scv3-filter-option'); 3979 if (label) { 3980 label.classList.remove('scv3-is-checked'); 3981 } 3982 }); 3983 filters.querySelectorAll('.scv3-filter-list--price input').forEach((input) => { 3984 input.value = ''; 3985 }); 3986 Page._state.urlParams = params; 3987 Page._state.currentPage = 1; 3988 Page.helpers.updateUrl(1); 3989 updateAll(); 3990 Page.mainBlocks.runFilters(); 3991 }; 3992 3993 const updateRoot = (root) => { 3994 const filters = getFilters(); 3995 const blocksContainer = root.querySelector('[data-scv3-filter-pills-blocks]'); 3996 const activeContainer = root.querySelector('[data-scv3-filter-pills-active]'); 3997 const countEl = root.querySelector('[data-scv3-filter-count]'); 3998 const filterBtn = root.querySelector('[data-scv3-filter-trigger]'); 3999 const resetBtn = root.querySelector('[data-scv3-filter-reset-all]'); 4000 4001 if (!filters) { 4002 if (blocksContainer) blocksContainer.innerHTML = ''; 4003 if (activeContainer) activeContainer.innerHTML = ''; 4004 if (filterBtn) filterBtn.classList.remove('scv3-is-active'); 4005 if (countEl) countEl.textContent = ''; 4006 if (resetBtn) resetBtn.classList.remove('scv3-is-visible'); 4007 return; 4008 } 4009 4010 buildBlockPills(filters, blocksContainer); 4011 const activeCount = buildActivePills(filters, activeContainer); 4012 if (filterBtn) { 4013 filterBtn.classList.toggle('scv3-is-active', activeCount > 0); 4014 } 4015 if (countEl) { 4016 countEl.textContent = activeCount > 0 ? String(activeCount) : ''; 4017 } 4018 if (resetBtn) { 4019 resetBtn.classList.toggle('scv3-is-visible', activeCount > 1); 4020 } 4021 }; 4022 4023 const updateAll = () => { 4024 roots.forEach(updateRoot); 4025 }; 4026 4027 roots.forEach((root) => { 4028 root.addEventListener('click', (e) => { 4029 const resetBtn = e.target.closest('[data-scv3-filter-reset-all]'); 4030 if (resetBtn) { 4031 e.preventDefault(); 4032 resetAllFilters(); 4033 return; 4034 } 4035 4036 const removeBtn = e.target.closest('[data-scv3-filter-remove]'); 4037 if (removeBtn) { 4038 const filters = getFilters(); 4039 if (!filters) return; 4040 if (removeBtn.dataset.scv3FilterType === 'price') { 4041 const inputs = Array.from(filters.querySelectorAll('.scv3-filter-list--price input')); 4042 inputs.forEach((input) => { 4043 input.value = ''; 4044 input.dispatchEvent(new Event('input', { bubbles: true })); 4045 input.dispatchEvent(new Event('change', { bubbles: true })); 4046 }); 4047 return; 4048 } 4049 if (removeBtn.dataset.scv3FilterType === 'categoryGroup') { 4050 const name = removeBtn.dataset.scv3FilterName || ''; 4051 const values = String(removeBtn.dataset.scv3FilterValues || '') 4052 .split(',') 4053 .map((v) => v.trim()) 4054 .filter((v) => v !== ''); 4055 if (!name || !values.length) return; 4056 let changedInput = null; 4057 values.forEach((value) => { 4058 const selector = `.scv3-filter-option input[name="${escSelector(name)}"][value="${escSelector(value)}"]`; 4059 const input = filters.querySelector(selector); 4060 if (!input || !input.checked) return; 4061 input.checked = false;
4062 input.indeterminate = false; 4063 const label = input.closest('.scv3-filter-option'); 4064 if (label) { 4065 label.classList.remove('scv3-is-checked'); 4066 } 4067 changedInput = input; 4068 }); 4069 if (changedInput) { 4070 changedInput.dispatchEvent(new Event('change', { bubbles: true })); 4071 } 4072 return; 4073 } 4074 const name = removeBtn.dataset.scv3FilterName || ''; 4075 const value = removeBtn.dataset.scv3FilterValue || ''; 4076 if (!name || value === '') return; 4077 const selector = `.scv3-filter-option input[name="${escSelector(name)}"][value="${escSelector(value)}"]`; 4078 const input = filters.querySelector(selector); 4079 if (input) { 4080 input.checked = false; 4081 input.dispatchEvent(new Event('change', { bubbles: true })); 4082 } 4083 return; 4084 } 4085 4086 const targetBtn = e.target.closest('[data-scv3-filter-target]'); 4087 if (targetBtn) { 4088 const slug = targetBtn.dataset.scv3FilterTarget || ''; 4089 if (typeof window.scv3OpenFilterAllSheet === 'function') { 4090 window.scv3OpenFilterAllSheet(slug, { fromFilters: false }); 4091 } else if (typeof window.scv3OpenFilterSheet === 'function') { 4092 window.scv3OpenFilterSheet(slug); 4093 } 4094 } 4095 }); 4096 }); 4097 4098 window.scv3UpdateMobileFilterPills = updateAll; 4099 updateAll(); 4100})(); 4101 4102window.addEventListener('popstate', () => { 4103 if (!Page || !Page.helpers) return; 4104 const routeState = Page.helpers.getRouteState(); 4105 Page._state.basePath = routeState.basePath; 4106 Page._state.currentPage = routeState.currentPage; 4107 Page._state.urlParams = routeState.urlParams; 4108 Page.helpers.updateSliderVisibility(); 4109 Page.helpers.fetchAndRender({ 4110 page: routeState.currentPage, 4111 append: false, 4112 withFacets: true, 4113 history: 'replace', 4114 updateUrl: false 4115 }); 4116}); 4117 4118(function() { 4119 const container = document.getElementById('tagsCloud'); 4120 if (!container) return; 4121 4122 const toggleBtn = container.querySelector('.tag-toggle'); 4123 const items = Array.from(container.querySelectorAll('.tag-item')); 4124 let isExpanded = false; 4125 let sizes = null; // { collapsed, full } 4126 4127// ÐамеÑÑем вÑÑоÑÑ ÑÑÑоки (ÑвÑÑнÑÑо) и полнÑÑ Ð²ÑÑоÑÑ 4128 function measure() { 4129// ÐÑе ÑлеменÑÑ Ð² поÑоке, кнопка на ÑвоÑм меÑÑе 4130 items.forEach(el => el.style.display = 'inline-block'); 4131 toggleBtn.style.display = 'inline-block'; 4132 toggleBtn.textContent = 'ÐÑе'; 4133 4134// ÐÑаниÑа пеÑвой ÑÑÑоки (как и ÑанÑÑе) 4135 const firstTop = items[0].offsetTop; 4136 let splitIndex = items.length - 1; 4137 for (let i = 0; i < items.length; i++) { 4138 items[i].after(toggleBtn); 4139 if ((toggleBtn.offsetTop - 20) > firstTop || items[i].offsetTop > firstTop) { 4140 splitIndex = i - 1; 4141 break; 4142 } 4143 } 4144 splitIndex = Math.max(0, splitIndex); 4145 items[splitIndex].after(toggleBtn); 4146 4147// ÐÑÑоÑа одной (пеÑвой) ÑÑÑоки: пÑÑÑем оÑÑалÑное ÑолÑко Ð´Ð»Ñ Ð·Ð°Ð¼ÐµÑа 4148 items.forEach((el, idx) => { el.style.display = (idx > splitIndex) ? 'none' : 'inline-block'; }); 4149 container.style.height = 'auto'; 4150 const collapsedH = container.clientHeight; 4151 4152// ÐÐ¾Ð»Ð½Ð°Ñ Ð²ÑÑоÑа 4153 items.forEach(el => el.style.display = 'inline-block'); 4154 container.style.height = 'auto'; 4155 const fullH = container.clientHeight; 4156 4157// ÐозвÑаÑаем вÑе в поÑок â пÑÑÑаÑÑ Ð±ÑÐ´ÐµÑ Ñам overflow: hidden 4158 items.forEach(el => el.style.display = 'inline-block'); 4159 4160 return { collapsed: collapsedH, full: fullH, hasOverflow: fullH > collapsedH }; 4161 } 4162 4163 function animateTo(h) { 4164// ÐÑновной пÑÑÑ â CSS transition (Ñже в ÑÑилÑÑ ). 4165// ÐÑли подклÑÑÑн jQuery â можно ÑаÑкомменÑиÑоваÑÑ ÑÑÑÐ¾ÐºÑ Ð½Ð¸Ð¶Ðµ. 4166// jQuery(container).stop().animate({ height: h + 'px' }, 350); 4167 container.style.height = h + 'px'; 4168 } 4169 4170 function recompute(animate) { 4171 sizes = measure(); 4172 4173 if (!sizes.hasOverflow) { 4174// ÐÑÑ Ð²Ð»ÐµÐ·Ð°ÐµÑ Ð² Ð¾Ð´Ð½Ñ ÑÑÑÐ¾ÐºÑ â кнопка не нÑжна, авÑо-вÑÑоÑа 4175 container.style.height = 'auto'; 4176 toggleBtn.style.display = 'none'; 4177 return; 4178 } 4179 4180 toggleBtn.style.display = 'inline-block'; 4181 toggleBtn.textContent = isExpanded ? 'СкÑÑÑÑ' : 'ÐÑе'; 4182 4183 const target = isExpanded ? sizes.full : sizes.collapsed; 4184 if (animate) animateTo(target); 4185 else container.style.height = target + 'px'; 4186 } 4187 4188 toggleBtn.addEventListener('click', function(e) { 4189 e.preventDefault(); 4190 isExpanded = !isExpanded; 4191 toggleBtn.textContent = isExpanded ? 'СкÑÑÑÑ' : 'ÐÑе'; 4192 animateTo(isExpanded ? sizes.full : sizes.collapsed); 4193 }); 4194 4195// ÐеÑеÑÑÑÑ Ð¿Ñи изменении ÑиÑÐ¸Ð½Ñ Ð¾ÐºÐ½Ð° (без анимаÑии, ÑÑÐ¾Ð±Ñ Ð½Ðµ дÑÑгалоÑÑ) 4196 let resizeTimeout; 4197 window.addEventListener('resize', function() { 4198 clearTimeout(resizeTimeout); 4199 resizeTimeout = setTimeout(function() { recompute(false); }, 120); 4200 }); 4201 4202 if (document.readyState === 'loading') { 4203 document.addEventListener('DOMContentLoaded', function() { recompute(false); }); 4204 } else { 4205 recompute(false); 4206 } 4207})();
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.