1const VISIBLE_COUNT = 4; 2 3/** Filter groups expanded via «ÑмоÑÑеÑÑ Ð²Ñе» â survive AJAX sidebar replace. */ 4const expandedFilterGroups = new Set(); 5 6/** «ÐолÑÑе ÑилÑÑÑов» block open state â null until first sync with DOM. */ 7let extraFiltersOpen = null; 8 9const isCatalogFilterLoading = () => document.querySelector(".catalog")?.dataset.filterLoading === "1"; 10 11const getFilterGroupKey = (filter) => filter?.dataset?.filterGroup || ""; 12 13const rememberExpandedFilter = (filter, isOpen) => { 14 const key = getFilterGroupKey(filter); 15 16 if (!key) { 17 return; 18 } 19 20 if (isOpen) { 21 expandedFilterGroups.add(key); 22 return; 23 } 24 25 expandedFilterGroups.delete(key); 26}; 27 28/** 29 * Snapshot currently open «ÑмоÑÑеÑÑ Ð²Ñе» groups before sidebar HTML replace. 30 */ 31export const captureExpandedFilterGroups = (catalog = document.querySelector(".catalog")) => { 32 catalog?.querySelectorAll(".catalog-sidebar .filter.open").forEach((filter) => { 33 const key = getFilterGroupKey(filter); 34 35 if (key) { 36 expandedFilterGroups.add(key); 37 } 38 }); 39 40 return [...expandedFilterGroups]; 41}; 42 43/** 44 * Remember whether «ÐолÑÑе ÑилÑÑÑов» is open before sidebar HTML replace. 45 */ 46export const captureExtraFiltersOpen = (catalog = document.querySelector(".catalog")) => { 47 const extra = catalog?.querySelector(".catalog-sidebar .filters-extra"); 48 49 if (!extra) { 50 return extraFiltersOpen; 51 } 52 53 extraFiltersOpen = extra.classList.contains("_open") && !extra.hasAttribute("hidden"); 54 return extraFiltersOpen; 55}; 56 57const applyExtraFiltersVisibility = (sidebar, open) => { 58 const extra = sidebar?.querySelector(".filters-extra"); 59 const btn = sidebar?.querySelector(".filters-more-btn"); 60 61 if (!extra || !btn) { 62 return; 63 } 64 65 if (open) { 66 extra.hidden = false; 67 extra.removeAttribute("hidden"); 68 extra.classList.add("_open"); 69 btn.textContent = btn.dataset.less || "ÐенÑÑе ÑилÑÑÑов"; 70 btn.setAttribute("aria-expanded", "true"); 71 // Heights were measured while display:none â max-height:0; recalc after show. 72 refreshFilterGroupHeights(extra); 73 return; 74 } 75 76 extra.hidden = true; 77 extra.setAttribute("hidden", ""); 78 extra.classList.remove("_open"); 79 btn.textContent = btn.dataset.more || "ÐолÑÑе ÑилÑÑÑов"; 80 btn.setAttribute("aria-expanded", "false"); 81}; 82 83const refreshFilterGroupHeights = (root) => { 84 if (!root) { 85 return; 86 } 87 88 root.querySelectorAll(".filter").forEach((filter) => { 89 if (filter.classList.contains("_price")) { 90 return; 91 } 92 93 const container = filter.querySelector(".filter-container"); 94 const checks = container ? Array.from(container.querySelectorAll(".check")) : []; 95 96 if (!container || !checks.length) { 97 return; 98 } 99 100 if (checks.length > VISIBLE_COUNT) { 101 if (filter.classList.contains("open") || expandedFilterGroups.has(getFilterGroupKey(filter))) { 102 setContainerHeight(container, filter, checks, "all", false); 103 return; 104 } 105 106 setContainerHeight(container, filter, checks, Math.min(VISIBLE_COUNT, checks.length), false); 107 return; 108 } 109 110 setContainerHeight(container, filter, checks, checks.length, false); 111 }); 112}; 113 114const syncExtraFiltersSection = (catalog) => { 115 const sidebar = catalog?.querySelector(".catalog-sidebar"); 116 117 if (!sidebar?.querySelector(".filters-extra")) { 118 return; 119 } 120 121 if (extraFiltersOpen === null) { 122 const extra = sidebar.querySelector(".filters-extra"); 123 extraFiltersOpen = extra.classList.contains("_open") && !extra.hasAttribute("hidden"); 124 } 125 126 applyExtraFiltersVisibility(sidebar, extraFiltersOpen); 127}; 128 129const getScrollbarWidth = () => window.innerWidth - document.documentElement.clientWidth; 130 131const lockBodyScroll = () => { 132 document.body.classList.add("_stopScroll"); 133 document.body.style.paddingRight = `${getScrollbarWidth()}px`; 134}; 135 136const unlockBodyScroll = () => { 137 document.body.classList.remove("_stopScroll"); 138 document.body.style.paddingRight = ""; 139}; 140 141/** 142 * Keep body scroll lock in sync with an open mobile catalog sidebar. 143 * Call after AJAX sidebar replace (node with `_show` is removed from DOM). 144 */ 145export const syncCatalogSidebarScrollLock = (catalog = document.querySelector(".catalog")) => { 146 const openSidebar = catalog?.querySelector(".catalog-sidebar._show"); 147 148 if (openSidebar) { 149 lockBodyScroll(); 150 return; 151 } 152 153 unlockBodyScroll(); 154}; 155 156const setChecksVisible = (checks, mode) => { 157 const limit = mode === "all" ? checks.length : mode; 158 159 checks.forEach((check, index) => { 160 if (mode === "all" || index < limit) { 161 check.classList.remove("hidden"); 162 check.style.cssText = ""; 163 return; 164 } 165 166 check.classList.add("hidden"); 167 check.style.cssText = ""; 168 }); 169}; 170 171const measureVisibleHeight = (checks, limit) => { 172 let height = 0; 173 174 checks.forEach((check, index) => { 175 if (index < limit) { 176 height += check.offsetHeight; 177 } 178 }); 179 180 return height; 181}; 182 183const setContainerHeight = (container, filter, checks, mode, animate = false) => { 184 filter.classList.remove("_animating"); 185 container.style.transition = "none"; 186 187 if (!animate) { 188 if (mode === "all") { 189 setChecksVisible(checks, checks.length); 190 container.style.maxHeight = `${container.scrollHeight}px`; 191 return; 192 } 193 194 setChecksVisible(checks, mode); 195 container.style.maxHeight = `${measureVisibleHeight(checks, mode)}px`; 196 return; 197 } 198 199 if (mode === "all") { 200 // Expand: reveal items under current clipped max-height, then animate open. 201 setChecksVisible(checks, checks.length); 202 const target = container.scrollHeight; 203 void container.offsetHeight; 204 filter.classList.add("_animating"); 205 container.style.maxHeight = `${target}px`; 206 return; 207 } 208 209 // Collapse: animate max-height first, hide overflow items after. 210 const target = measureVisibleHeight(checks, mode); 211 container.style.maxHeight = `${container.scrollHeight}px`; 212 void container.offsetHeight; 213 filter.classList.add("_animating"); 214 container.style.maxHeight = `${target}px`; 215 216 const onEnd = (event) => { 217 if (event.target !== container || event.propertyName !== "max-height") { 218 return; 219 } 220 221 container.removeEventListener("transitionend", onEnd); 222 filter.classList.remove("_animating"); 223 setChecksVisible(checks, mode); 224 container.style.maxHeight = `${measureVisibleHeight(checks, mode)}px`; 225 }; 226 227 container.addEventListener("transitionend", onEnd); 228}; 229 230const initPriceFilter = (filter) => { 231 const container = filter.querySelector(".filter-container"); 232 const resetBtn = filter.querySelector(".filter-reset"); 233 const titleEl = filter.querySelector(".filter-title"); 234 const defaultTitle = titleEl?.textContent ?? ""; 235 236 if (titleEl) { 237 titleEl.dataset.defaultTitle = defaultTitle; 238 } 239 240 resetBtn?.addEventListener("click", () => { 241 if (isCatalogFilterLoading()) { 242 return; 243 } 244 245 container?.querySelectorAll(".check__input").forEach((input) => { 246 input.checked = false; 247 }); 248 249 if (titleEl) { 250 titleEl.textContent = defaultTitle; 251 } 252 253 document.dispatchEvent(new CustomEvent("photohuman:catalog-filter-appl
253y")); 254 }); 255}; 256 257const initStandardFilter = (filter) => { 258 const container = filter.querySelector(".filter-container"); 259 const checks = container ? Array.from(container.querySelectorAll(".check")) : []; 260 const moreBtn = filter.querySelector(".filter-more"); 261 const resetBtn = filter.querySelector(".filter-reset"); 262 const titleEl = filter.querySelector(".filter-title"); 263 const rawTitle = titleEl?.textContent?.trim() ?? ""; 264 const defaultTitle = rawTitle.replace(/\s*\[\d+\]\s*$/u, "").trim() || rawTitle; 265 const moreLabelDefault = moreBtn?.dataset.default || moreBtn?.textContent.trim() || "ÑмоÑÑеÑÑ Ð²Ñе"; 266 const moreLabelLess = moreBtn?.dataset.less || "ÑвеÑнÑÑÑ"; 267 268 if (!container || !checks.length) { 269 return; 270 } 271 272 if (titleEl) { 273 titleEl.dataset.defaultTitle = defaultTitle; 274 } 275 276 const updateTitle = () => { 277 if (!titleEl) { 278 return; 279 } 280 281 const checkedCount = container.querySelectorAll(".check__input:checked").length; 282 283 titleEl.textContent = 284 checkedCount > 0 ? `${defaultTitle} [${checkedCount}]` : defaultTitle; 285 }; 286 287 const applyCollapsedHeight = (animate = false) => { 288 setContainerHeight(container, filter, checks, Math.min(VISIBLE_COUNT, checks.length), animate); 289 }; 290 291 const applyExpandedHeight = (animate = false) => { 292 setContainerHeight(container, filter, checks, "all", animate); 293 }; 294 295 if (checks.length > VISIBLE_COUNT) { 296 filter.classList.add("with-more"); 297 298 if (moreBtn) { 299 moreBtn.hidden = false; 300 moreBtn.style.display = ""; 301 moreBtn.dataset.default = moreLabelDefault; 302 } 303 304 const keepExpanded = expandedFilterGroups.has(getFilterGroupKey(filter)); 305 306 if (keepExpanded) { 307 filter.classList.add("open"); 308 applyExpandedHeight(false); 309 310 if (moreBtn) { 311 moreBtn.textContent = moreLabelLess; 312 } 313 } else { 314 filter.classList.remove("open"); 315 applyCollapsedHeight(false); 316 317 if (moreBtn) { 318 moreBtn.textContent = moreLabelDefault; 319 } 320 } 321 } else { 322 filter.classList.remove("with-more", "open"); 323 rememberExpandedFilter(filter, false); 324 325 if (moreBtn) { 326 moreBtn.hidden = true; 327 moreBtn.style.display = "none"; 328 } 329 330 setContainerHeight(container, filter, checks, checks.length, false); 331 } 332 333 moreBtn?.addEventListener("click", () => { 334 if (filter.classList.contains("open")) { 335 filter.classList.remove("open"); 336 rememberExpandedFilter(filter, false); 337 applyCollapsedHeight(true); 338 moreBtn.textContent = moreLabelDefault; 339 return; 340 } 341 342 filter.classList.add("open"); 343 rememberExpandedFilter(filter, true); 344 applyExpandedHeight(true); 345 moreBtn.textContent = moreLabelLess; 346 }); 347 348 resetBtn?.addEventListener("click", () => { 349 if (isCatalogFilterLoading()) { 350 return; 351 } 352 353 container.querySelectorAll(".check__input").forEach((input) => { 354 input.checked = false; 355 }); 356 updateTitle(); 357 document.dispatchEvent(new CustomEvent("photohuman:catalog-filter-apply")); 358 }); 359 360 container.addEventListener("change", (event) => { 361 if (event.target instanceof HTMLInputElement && event.target.classList.contains("check__input")) { 362 updateTitle(); 363 } 364 }); 365 366 window.addEventListener("resize", () => { 367 if (filter.classList.contains("open")) { 368 applyExpandedHeight(false); 369 return; 370 } 371 372 if (checks.length > VISIBLE_COUNT) { 373 applyCollapsedHeight(false); 374 return; 375 } 376 377 setContainerHeight(container, filter, checks, checks.length, false); 378 }); 379 380 updateTitle(); 381}; 382 383const resetAllCatalogFilters = (catalog) => { 384 if (isCatalogFilterLoading()) { 385 return; 386 } 387 388 const sidebar = catalog?.querySelector(".catalog-sidebar"); 389 390 if (!sidebar) { 391 return; 392 } 393 394 sidebar.querySelectorAll(".filter .check__input").forEach((input) => { 395 input.checked = false; 396 }); 397 398 sidebar.querySelectorAll(".filter.open").forEach((filter) => { 399 filter.classList.remove("open"); 400 rememberExpandedFilter(filter, false); 401 }); 402 expandedFilterGroups.clear(); 403 404 sidebar.querySelectorAll(".filter-more").forEach((moreBtn) => { 405 if (moreBtn.dataset.default) { 406 moreBtn.textContent = moreBtn.dataset.default; 407 } 408 }); 409 410 sidebar.querySelectorAll(".filter-title").forEach((titleEl) => { 411 if (titleEl.dataset.defaultTitle) { 412 titleEl.textContent = titleEl.dataset.defaultTitle; 413 } 414 }); 415 416 document.dispatchEvent(new CustomEvent("photohuman:catalog-filter-appl
416y")); 417}; 418 419const initCatalogSidebar = (catalog) => { 420 if (!catalog || catalog.dataset.catalogSidebarToggleBound === "1") { 421 return; 422 } 423 424 // Live query: AJAX filter replace removes the sidebar node from DOM. 425 catalog.dataset.catalogSidebarToggleBound = "1"; 426 427 document.body.addEventListener("click", (event) => { 428 const target = event.target; 429 430 if (!(target instanceof Element) || !catalog.contains(target)) { 431 return; 432 } 433 434 const sidebar = catalog.querySelector(".catalog-sidebar"); 435 436 if (!sidebar) { 437 return; 438 } 439 440 if (target.closest(".filters-more-btn")) { 441 event.preventDefault(); 442 extraFiltersOpen = !extraFiltersOpen; 443 applyExtraFiltersVisibility(sidebar, extraFiltersOpen); 444 return; 445 } 446 447 if (target.closest(".catalog-head__filter")) { 448 event.preventDefault(); 449 sidebar.classList.add("_show"); 450 lockBodyScroll(); 451 return; 452 } 453 454 if (target.closest(".sidebar-close")) { 455 event.preventDefault(); 456 sidebar.classList.remove("_show"); 457 syncCatalogSidebarScrollLock(catalog); 458 459 window.setTimeout(() => { 460 const sidebarBody = sidebar.querySelector(".sidebar-body"); 461 462 if (sidebarBody) { 463 sidebarBody.scrollTop = 0; 464 } 465 }, 400); 466 } 467 }); 468}; 469 470const initFilterGroups = (catalog) => { 471 const sidebarBody = catalog.querySelector(".sidebar-body"); 472 473 if (!sidebarBody) { 474 return; 475 } 476 477 // Init primary filters first (visible). Extra block may be display:none â 478 // its heights are refreshed in syncExtraFiltersSection when opened. 479 sidebarBody.querySelectorAll(".filter").forEach((filter) => { 480 if (filter.classList.contains("_price")) { 481 initPriceFilter(filter); 482 return; 483 } 484 485 initStandardFilter(filter); 486 }); 487 488 syncExtraFiltersSection(catalog); 489}; 490 491export const initCatalogFilter = () => { 492 const catalog = document.querySelector(".catalog"); 493 494 if (!catalog) { 495 return; 496 } 497 498 if (!catalog.dataset.catalogSidebarBound) { 499 catalog.dataset.catalogSidebarBound = "1"; 500 501 catalog.addEventListener("click", (event) => { 502 const target = event.target; 503 504 if (!(target instanceof Element)) { 505 return; 506 } 507 508 if (target.closest(".sidebar-button .button")) { 509 event.preventDefault(); 510 resetAllCatalogFilters(catalog); 511 } 512 }); 513 } 514 515 initCatalogSidebar(catalog); 516 initFilterGroups(catalog); 517}; 518 519export const reinitCatalogFilter = () => { 520 const catalog = document.querySelector(".catalog"); 521 522 if (!catalog) { 523 return; 524 } 525 526 initFilterGroups(catalog); 527};
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.