1import { serverReq, block, unblock, ICON_ARROW_HTML, setButtonLabel } from "./helpers.js"; 2import { reinitCatalogFilter, syncCatalogSidebarScrollLock, captureExpandedFilterGroups, captureExtraFiltersOpen } from "./catalogFilter.js?v=1.6.797"; 3 4const FILTER_PARAM_KEYS = [ 5 "filter_interior_style", 6 "filter_cat", 7 "filter_color_gamut", 8 "filter_product_medium", 9 "filter_product_format", 10 "filter_product_elements", 11 "filter_product_type", 12 "filter_year_of_horse", 13 "filter_product_price", 14 "filter_author", 15 "filter_awards", 16 "filter_product_edition_range", 17]; 18 19const DEFAULT_CATALOG_SORT = "date"; 20 21/** Ignore outside-close briefly after open â mobile ghost pointerdown closes the menu instantly. */ 22let catalogSortIgnoreOutsideUntil = 0; 23 24const armCatalogSortOutsideIgnore = (ms = 450) => { 25 catalogSortIgnoreOutsideUntil = Date.now() + ms; 26}; 27 28const getCatalog = () => document.querySelector(".catalog"); 29 30const getCatalogSortRoot = (catalog = getCatalog()) => 31 catalog?.querySelector("[data-catalog-sort-wrap]"); 32 33const getCatalogSortInput = (catalog = getCatalog()) => 34 catalog?.querySelector("[data-catalog-sort]"); 35 36const getCatalogSortCycleBtn = (catalog = getCatalog()) => 37 catalog?.querySelector("[data-catalog-sort-cycle]"); 38 39const isCatalogSortMobile = () => window.matchMedia("(max-width: 1023px)").matches; 40 41const getCatalogSortValue = (catalog = getCatalog()) => { 42 const input = getCatalogSortInput(catalog); 43 44 if (!(input instanceof HTMLInputElement) && !(input instanceof HTMLSelectElement)) { 45 return DEFAULT_CATALOG_SORT; 46 } 47 48 const value = (input.value || input.getAttribute("value") || "").trim(); 49 50 return value || DEFAULT_CATALOG_SORT; 51}; 52 53const syncCatalogSortCycleBtn = (catalog, value) => { 54 const btn = getCatalogSortCycleBtn(catalog); 55 56 if (!(btn instanceof HTMLElement)) { 57 return; 58 } 59 60 const next = value || DEFAULT_CATALOG_SORT; 61 const activeItem = getCatalogSortRoot(catalog)?.querySelector( 62 `.select-list li[data-id="${CSS.escape(next)}"]`, 63 ); 64 const label = activeItem?.textContent?.trim() || next; 65 66 btn.dataset.sort = next; 67 btn.setAttribute("aria-label", `СоÑÑиÑовка: ${label}`); 68}; 69 70const closeCatalogSortMenu = (catalog = getCatalog()) => { 71 const root = getCatalogSortRoot(catalog); 72 const btn = getCatalogSortCycleBtn(catalog); 73 const head = root?.querySelector(".select-head"); 74 75 if (!(root instanceof HTMLElement)) { 76 return; 77 } 78 79 root.classList.remove("_show"); 80 root.querySelector(".select-container")?.removeAttribute("style"); 81 root.removeAttribute("style"); 82 btn?.setAttribute("aria-expanded", "false"); 83 head?.setAttribute("aria-expanded", "false"); 84}; 85 86const openCatalogSortMenu = (catalog = getCatalog()) => { 87 const root = getCatalogSortRoot(catalog); 88 const btn = getCatalogSortCycleBtn(catalog); 89 const head = root?.querySelector(".select-head"); 90 const container = root?.querySelector(".select-container"); 91 const list = root?.querySelector(".select-list"); 92 93 if (!(root instanceof HTMLElement) || !(container instanceof HTMLElement) || !(list instanceof HTMLElement)) { 94 return; 95 } 96 97 root.classList.add("_show"); 98 container.style.height = `${Math.max(list.scrollHeight, list.offsetHeight)}px`; 99 root.style.zIndex = "40"; 100 btn?.setAttribute("aria-expanded", "true"); 101 head?.setAttribute("aria-expanded", "true"); 102 armCatalogSortOutsideIgnore(); 103}; 104 105const toggleCatalogSortMenu = (catalog = getCatalog()) => { 106 const root = getCatalogSortRoot(catalog); 107 108 if (!(root instanceof HTMLElement) || isCatalogFilterLoading(catalog)) { 109 return; 110 } 111 112 if (root.classList.contains("_show")) { 113 closeCatalogSortMenu(catalog); 114 return; 115 } 116 117 openCatalogSortMenu(catalog); 118}; 119 120const setCatalogSortValue = (catalog, value) => { 121 const root = getCatalogSortRoot(catalog); 122 const input = getCatalogSortInput(catalog); 123 124 if (!root || !(input instanceof HTMLInputElement)) { 125 syncCatalogSortCycleBtn(catalog, value || DEFAULT_CATALOG_SORT); 126 return; 127 } 128 129 const next = value || DEFAULT_CATALOG_SORT; 130 const items = [...root.querySelectorAll(".select-list li[data-id]")]; 131 const activeItem = items.find((item) => item.getAttribute("data-id") === next) || items[0]; 132 133 if (!activeItem) { 134 syncCatalogSortCycleBtn(catalog, next); 135 return; 136 } 137 138 const nextValue = activeItem.getAttribute("data-id") || DEFAULT_CATALOG_SORT; 139 input.value = nextValue; 140 input.setAttribute("value", nextValue); 141 142 const text = root.querySelector(".select-head__text"); 143 144 if (text) { 145 text.textContent = activeItem.textContent.trim(); 146 text.classList.remove("_placeholder"); 147 } 148
149 items.forEach((item) => { 150 const isActive = item === activeItem; 151 item.classList.toggle("_active", isActive); 152 item.setAttribute("aria-selected", isActive ? "true" : "false"); 153 }); 154 155 syncCatalogSortCycleBtn(catalog, nextValue); 156}; 157 158const isCatalogFilterLoading = (catalog = getCatalog()) => catalog?.dataset.filterLoading === "1"; 159 160const setCatalogFilterLoading = (catalog, loading) => { 161 if (!catalog) { 162 return; 163 } 164 165 if (loading) { 166 catalog.dataset.filterLoading = "1"; 167 return; 168 } 169 170 delete catalog.dataset.filterLoading; 171}; 172 173const getCatalogBlockContainer = (catalog) => 174 catalog.querySelector(".catalog-box._to_block") || 175 catalog.querySelector(".catalog-content") || 176 catalog.querySelector(".catalog-container"); 177 178const getCatalogSidebarBlockTarget = (catalog) => catalog?.querySelector(".catalog-sidebar .sidebar"); 179 180const blockCatalogFilters = (catalog) => { 181 const sidebar = getCatalogSidebarBlockTarget(catalog); 182 const blockContainer = getCatalogBlockContainer(catalog); 183 const filterToggle = catalog?.querySelector(".catalog-head__filter"); 184 const sortRoot = getCatalogSortRoot(catalog); 185 const sortCycleBtn = getCatalogSortCycleBtn(catalog); 186 187 if (sidebar) { 188 sidebar.classList.add("_to_block"); 189 block(sidebar); 190 } 191 192 if (blockContainer) { 193 block(blockContainer); 194 } 195 196 if (filterToggle instanceof HTMLElement) { 197 filterToggle.style.pointerEvents = "none"; 198 filterToggle.setAttribute("aria-disabled", "true"); 199 } 200 201 if (sortRoot instanceof HTMLElement) { 202 sortRoot.classList.add("is-disabled"); 203 sortRoot.classList.remove("_show"); 204 sortRoot.querySelector(".select-container")?.removeAttribute("style"); 205 } 206 207 if (sortCycleBtn instanceof HTMLElement) { 208 sortCycleBtn.classList.add("is-disabled"); 209 sortCycleBtn.setAttribute("aria-disabled", "true"); 210 } 211}; 212 213const unblockCatalogFilters = (catalog) => { 214 const sidebar = getCatalogSidebarBlockTarget(catalog); 215 const blockContainer = getCatalogBlockContainer(catalog); 216 const filterToggle = catalog?.querySelector(".catalog-head__filter"); 217 const sortRoot = getCatalogSortRoot(catalog); 218 const sortCycleBtn = getCatalogSortCycleBtn(catalog); 219 220 if (sidebar) { 221 unblock(sidebar); 222 sidebar.classList.remove("_to_block"); 223 } 224 225 if (blockContainer) { 226 unblock(blockContainer); 227 } 228 229 if (filterToggle instanceof HTMLElement) { 230 filterToggle.style.pointerEvents = ""; 231 filterToggle.removeAttribute("aria-disabled"); 232 } 233 234 if (sortRoot instanceof HTMLElement) { 235 sortRoot.classList.remove("is-disabled"); 236 } 237 238 if (sortCycleBtn instanceof HTMLElement) { 239 sortCycleBtn.classList.remove("is-disabled"); 240 sortCycleBtn.removeAttribute("aria-disabled"); 241 } 242}; 243 244const appendCatalogContextToFormData = (data, catalog) => { 245 if (!catalog) { 246 return; 247 } 248 249 if (catalog.dataset.contextCat) { 250 data.append("context_cat", catalog.dataset.contextCat); 251 } 252 253 if (catalog.dataset.contextPodborki) { 254 data.append("context_podborki", catalog.dataset.contextPodborki); 255 } 256 257 if (catalog.dataset.contextPodborkiAll) { 258 data.append("context_podborki_all", "1"); 259 } 260 261 if (catalog.dataset.contextGiftIdea) { 262 data.append("context_gift_idea", catalog.dataset.contextGiftIdea); 263 } 264}; 265 266const collectCatalogFilters = (catalog) => { 267 const data = new FormData(); 268 const sidebar = catalog?.querySelector(".catalog-sidebar"); 269 270 if (sidebar) { 271 sidebar.querySelectorAll(".check__input:checked").forEach((input) => { 272 if (input.disabled) { 273 return; 274 } 275 276 const filterKey = input.dataset.filterGroup; 277 278 if (filterKey) { 279 data.append(`${filterKey}[]`, input.value); 280 return; 281 } 282 283 if (input.name) { 284 data.append(input.name, input.value); 285 } 286 }); 287 } 288 289 data.append("catalog_sort", getCatalogSortValue(catalog)); 290 appendCatalogContextToFormData(data, catalog); 291 292 return data; 293}; 294 295const buildUrlSearchParams = (catalog) => { 296 const params = new URLSearchParams(); 297 const sidebar = catalog?.querySelector(".catalog-sidebar"); 298 299 if (sidebar) { 300 FILTER_PARAM_KEYS.forEach((key) => { 301 sidebar.querySelectorAll(`input[name="${key}[]"]:checked`).forEach((input) => { 302 params.append(`${key}[]`, input.value); 303 }); 304 }); 305 } 306 307 const sort = getCatalogSortValue(catalog); 308 309 if (sort && sort !== DEFAULT_CATALOG_SORT) { 310 params.set("catalog_sort", sort); 311 } 312 313 if (catalog?.dataset.contextCat) { 314 params.set("context_cat", catalog.dataset.contextCat); 315 } 316 317 if (catalog?.dataset.contextPodborki) { 318 params.set("context_podborki", catalog.dataset.contextPodborki); 319 } 320 321 if (catalog?.dataset.contextPodborkiAll) { 322 params.set("context_podborki_all", "1"); 323 } 324 325 if (catalog?.dataset.contextGiftIdea) { 326 params.set("context_gift_idea", catalog.dataset.contextGiftIdea); 327 } 328 329 return params; 330}; 331 332const syncUrl = (catalog) => { 333 const params = buildUrlSearchParams(catalog); 334 const query = params.toString(); 335 const nextUrl = query ? `${window.location.pathname}?${query}` : window.location.pathname; 336 337 window.history.pushState({ catalogFilters: true }, "", nextUrl); 338}; 339 340const applyFiltersFromUrl = (catalog) => { 341 restoreSidebarFilters(catalog, new URLSearchParams(window.location.search)); 342}; 343 344const restoreSidebarFilters = (catalog, params) => { 345 const sidebar = catalog?.querySelector(".catalog-sidebar"); 346 347 setCatalogSortValue(catalog, params.get("catalog_sort") || DEFAULT_CATALOG_SORT); 348 349 if (!sidebar) { 350 return; 351 } 352 353 sidebar.querySelectorAll(".check__input").forEach((input) => { 354 input.checked = false; 355 }); 356
357 FILTER_PARAM_KEYS.forEach((key) => { 358 // Accept both ?filter_x=slug (mega-menu / share links) and ?filter_x[]=slug (AJAX syncUrl). 359 const values = [...new Set([...params.getAll(`${key}[]`), ...params.getAll(key)])]; 360 361 values.forEach((value) => { 362 const input = sidebar.querySelector(`input[name="${key}[]"][value="${CSS.escape(value)}"]`); 363 if (input && !input.disabled) { 364 input.checked = true; 365 } 366 }); 367 }); 368 369 sidebar.querySelectorAll(".filter").forEach((filter) => { 370 const titleEl = filter.querySelector(".filter-title"); 371 const container = filter.querySelector(".filter-container"); 372 373 if (!titleEl || !container || !titleEl.dataset.defaultTitle) { 374 return; 375 } 376 377 const checkedCount = container.querySelectorAll(".check__input:checked").length; 378 379 titleEl.textContent = 380 checkedCount > 0 ? `${titleEl.dataset.defaultTitle} [${checkedCount}]` : titleEl.dataset.defaultTitle; 381 }); 382}; 383 384const updateCatalogUi = (catalog, res) => { 385 const catalogContainer = catalog.querySelector(".catalog-container"); 386 const catalogBox = catalog.querySelector(".catalog-box"); 387 const infoText = 388 catalog.querySelector(".catalog-head__found") || catalog.querySelector(".catalog-head__info p"); 389 390 if (res.is_empty) { 391 catalogBox?.remove(); 392 catalog.querySelector(".catalog-more")?.remove(); 393 394 let catalogContent = catalog.querySelector(".catalog-content"); 395 396 if (res.empty_html) { 397 if (catalogContent) { 398 catalogContent.outerHTML = res.empty_html; 399 } else { 400 catalog.querySelector(".catalog-head")?.insertAdjacentHTML("afterend", res.empty_html); 401 } 402 } 403 } else { 404 catalog.querySelector(".catalog-content")?.remove(); 405 406 if (!catalogBox && catalogContainer) { 407 catalog.querySelector(".catalog-head")?.insertAdjacentHTML( 408 "afterend", 409 '<div class="catalog-box _to_block"></div>', 410 ); 411 } 412 413 const activeCatalogBox = catalog.querySelector(".catalog-box"); 414 415 if (activeCatalogBox) { 416 activeCatalogBox.hidden = false; 417 activeCatalogBox.innerHTML = res.html || ""; 418 } 419 420 window.photohumanMatchOperationCardHeights?.(); 421 } 422 423 if (infoText) { 424 if (res.has_active_filters && res.found_label) { 425 infoText.textContent = res.found_label; 426 infoText.hidden = false; 427 } else { 428 infoText.hidden = true; 429 } 430 } 431 const moreWrap = catalog.querySelector(".catalog-more"); 432 const activeFilterParams = res.facets_html ? buildUrlSearchParams(catalog) : null; 433 434 if (res.has_more) { 435 if (!moreWrap) { 436 const container = catalog.querySelector(".catalog-container"); 437 container?.insertAdjacentHTML( 438 "beforeend", 439 `<div class="catalog-more"><button type="button" class="button _black" data-click="loadProduct" data-page="2">ÐÐÐÐÐÐТЬ ÐЩÐ${ICON_ARROW_HTML}</button></div>`, 440 ); 441 } 442 443 const btn = catalog.querySelector(".catalog-more .button[data-click='loadProduct']"); 444 if (btn) { 445 btn.dataset.page = "2"; 446 btn.dataset.total = String(res.total ?? ""); 447 btn.disabled = false; 448 setButtonLabel(btn, "ÐÐÐÐÐÐТЬ ÐЩÐ", true); 449 } 450 } else { 451 moreWrap?.remove(); 452 } 453 454 if (res.facets_html) { 455 updateCatalogSidebarFacets(catalog, res.facets_html, activeFilterParams); 456 } 457}; 458 459/** 460 * Refresh filter options inside the existing drawer. 461 * Keeps `.catalog-sidebar` (and `_show`) so the mobile modal does not flash. 462 */ 463const updateCatalogSidebarFacets = (catalog, facetsHtml, activeFilterParams = null) => { 464 const currentSidebar = catalog.querySelector(".catalog-sidebar"); 465 466 if (!currentSidebar) { 467 return; 468 } 469 470 const template = document.createElement("template"); 471 template.innerHTML = String(facetsHtml || "").trim(); 472 const nextSidebar = template.content.querySelector(".catalog-sidebar"); 473 const nextInner = nextSidebar?.querySelector(":scope > .sidebar"); 474 const currentInner = currentSidebar.querySelector(":scope > .sidebar"); 475 const scrollTop = currentSidebar.querySelector(".sidebar-body")?.scrollTop || 0; 476 477 captureExpandedFilterGroups(catalog); 478 captureExtraFiltersOpen(catalog); 479 480 if (nextInner && currentInner) { 481 currentInner.replaceWith(nextInner); 482 } else if (nextSidebar) { 483 // Fallback: rare malformed response â replace the whole drawer. 484 const wasOpen = currentSidebar.classList.contains("_show"); 485 currentSidebar.replaceWith(nextSidebar); 486 487 if (wasOpen) { 488 nextSidebar.classList.add("_show"); 489 } 490 } 491 492 const sidebarBody = catalog.querySelector(".catalog-sidebar .sidebar-body"); 493 494 if (sidebarBody) { 495 sidebarBody.scrollTop = scrollTop; 496 } 497 498 syncCatalogSidebarScrollLock(catalog); 499 reinitCatalogFilter(); 500 restoreSidebarFilters(catalog, activeFilterParams ?? buildUrlSearchParams(catalog)); 501}; 502 503const applyCatalogFilters = () => { 504 const catalog = getCatalog(); 505 506 if (!catalog || isCatalogFilterLoading(catalog)) { 507 return; 508 } 509 510 setCatalogFilterLoading(catalog, true); 511 blockCatalogFilters(catalog); 512 513 const formData = collectCatalogFilters(catalog); 514 515 formData.append("page", "1"); 516 517 serverReq(formData, "filterCatalog") 518 .then((res) => { 519 if (!res?.success) { 520 return; 521 } 522 523 updateCatalogUi(catalog, res); 524 syncUrl(catalog); 525 }) 526 .finally(() => { 527 setCatalogFilterLoading(catalog, false); 528 unblockCatalogFilters(catalog); 529 }); 530}; 531 532const handleSidebarFilterChange = (event) => { 533 const target = event.target; 534 const catalog = getCatalog(); 535 536 if (!(target instanceof HTMLInputElement) || !target.classList.contains("check__input")) { 537 return; 538 } 539 540 if (!target.closest(".catalog-sidebar") || target.disabled) { 541 return; 542 } 543 544 if (isCatalogFilterLoading(catalog)) { 545 target.checked = !target.checked; 546 return; 547 } 548 549 applyCatalogFilters(); 550}; 551 552const handleCatalogSortPick = (event) => { 553 const target = event.target; 554 const catalog = getCatalog(); 555 556 if (!(target instanceof Element) || !catalog) { 557 return; 558 } 559 560 const item = target.closest(".catalog-sort .select-list li[data-id]"); 561 562 if (!item || !item.closest(".catalog")) { 563 return; 564 } 565 566 event.preventDefault(); 567 event.stopPropagation(); 568 569 if (isCatalogFilterLoading(catalog)) { 570 return; 571 } 572 573 const next = item.getAttribute("data-id") || DEFAULT_CATALOG_SORT; 574 const prev = getCatalogSortValue(catalog); 575 576 setCatalogSortValue(catalog, next); 577 closeCatalogSortMenu(catalog); 578 579 if (next === prev) { 580 return; 581 } 582 583 applyCatalogFilters(); 584}; 585 586const handleCatalogSortToggle = (event) => { 587 const target = event.target; 588 const catalog = getCatalog(); 589 590 if (!(target instanceof Element) || !catalog) { 591 return; 592 } 593 594 const mobileBtn = target.closest("[data-catalog-sort-cycle]"); 595 const desktopHead = target.closest(".catalog-sort .select-head"); 596 597 if (!mobileBtn && !desktopHead) { 598 return; 599 } 600 601 if (!target.closest(".catalog")) { 602 return; 603 } 604 605 // Don't toggle when clicking the hidden input inside the head. 606 if (target instanceof HTMLInputElement && target.matches("[data-catalog-sort]")) { 607 return; 608 } 609 610 event.preventDefault(); 611 event.stopPropagation(); 612 613 toggleCatalogSortMenu(catalog); 614}; 615 616const handleCatalogSortOutsidePointer = (event) => { 617 if (Date.now() < catalogSortIgnoreOutsideUntil) { 618 return; 619 } 620 621 const catalog = getCatalog(); 622 const target = event.target; 623 624 if (!catalog || !(target instanceof Node)) { 625 return; 626 } 627 628 const root = getCatalogSortRoot(catalog); 629 630 if (!(root instanceof HTMLElement) || !root.classList.contains("_show")) { 631 return; 632 } 633 634 if (root.contains(target)) { 635 return; 636 } 637 638 closeCatalogSortMenu(catalog); 639}; 640 641export const initApplyCatalogFilter = () => { 642 const catalog = getCatalog(); 643 644 if (!catalog) { 645 return; 646 } 647 648 if (window.location.search) { 649 applyFiltersFromUrl(catalog); 650 } 651 652 syncCatalogSortCycleBtn(catalog, getCatalogSortValue(catalog)); 653 getCatalogSortCycleBtn(catalog)?.setAttribute("aria-expanded", "false"); 654 getCatalogSortRoot(catalog)?.querySelector(".select-head")?.setAttribute("aria-expanded", "false"); 655 656 catalog.addEventListener("change", handleSidebarFilterChange); 657 catalog.addEventListener("click", handleCatalogSortPick); 658 catalog.addEventListener("click", handleCatalogSortToggle); 659 // Capture pointerdown closes on outside tap; ignore-window prevents openâinstant-close on mobile. 660 document.addEventListener("pointerdown", handleCatalogSortOutsidePointer, true); 661
662 document.addEventListener("photohuman:catalog-filter-apply", applyCatalogFilters); 663 664 window.addEventListener("popstate", () => { 665 const currentCatalog = getCatalog(); 666 667 if (!currentCatalog) { 668 return; 669 } 670 671 applyFiltersFromUrl(currentCatalog); 672 applyCatalogFilters(); 673 }); 674 675 // Close only when crossing desktop/mobile breakpoint â not on mobile chrome resize. 676 let sortMq = window.matchMedia("(max-width: 1023px)"); 677 const onSortMqChange = () => closeCatalogSortMenu(getCatalog()); 678 if (typeof sortMq.addEventListener === "function") { 679 sortMq.addEventListener("change", onSortMqChange); 680 } else if (typeof sortMq.addListener === "function") { 681 sortMq.addListener(onSortMqChange); 682 } 683}; 684 685export const appendCatalogFiltersToFormData = (formData, catalog = getCatalog()) => { 686 if (!catalog) { 687 return formData; 688 } 689 690 const filters = collectCatalogFilters(catalog); 691 692 for (const [key, value] of filters.entries()) { 693 formData.append(key, value); 694 } 695 696 return formData; 697};
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.