PageSourceSearch

https://slidy.ai/assets/js/catalog.js?v=c805ed49109506f9

js slidy.ai collected 2026-10-05 12:38:15 UTC 12,402 bytes, 399 lines download raw bytes

1(function () {
2  function initSlider() {
3    const minRange = document.getElementById("minRange");
4    const maxRange = document.getElementById("maxRange");
5    const minLabel = document.getElementById("minLabel");
6    const maxLabel = document.getElementById("maxLabel");
7    const activeTrack = document.getElementById("activeTrack");
8
9    // если блока нет на странице — просто выходим, ошибок не будет
10    if (!minRange || !maxRange || !minLabel || !maxLabel || !activeTrack) {
11      return false;
12    }
13
14    // защита от повторной инициализации, если функция вызовется ещё раз
15    if (minRange.dataset.sliderInit) {
16      return true;
17    }
18    minRange.dataset.sliderInit = "true";
19
20    const gap = 1; // минимальное расстояние между ползунками
21
22    function updateSlider(e) {
23      let minVal = parseInt(minRange.value, 10);
24      let maxVal = parseInt(maxRange.value, 10);
25
26      const triggeredBy = e ? e.target : null;
27
28      // не даём ползункам пересекаться / меняться местами
29      if (maxVal - minVal < gap) {
30        if (triggeredBy === minRange) {
31          minRange.value = maxVal - gap;
32          minVal = maxVal - gap;
33        } else {
34          maxRange.value = minVal + gap;
35          maxVal = minVal + gap;
36        }
37      }
38
39      minLabel.textContent = minVal;
40      maxLabel.textContent = maxVal;
41
42      const rangeMin = parseInt(minRange.min, 10);
43      const rangeMax = parseInt(minRange.max, 10);
44
45      const percentMin = ((minVal - rangeMin) / (rangeMax - rangeMin)) * 100;
46      const percentMax = ((maxVal - rangeMin) / (rangeMax - rangeMin)) * 100;
47
48      activeTrack.style.left = percentMin + "%";
49      activeTrack.style.right = 100 - percentMax + "%";
50    }
51
52    minRange.addEventListener("input", updateSlider);
53    maxRange.addEventListener("input", updateSlider);
54
55    updateSlider();
56    return true;
57  }
58
59  // если блок уже есть при загрузке
60  function tryInit() {
61    if (document.readyState === "loading") {
62      document.addEventListener("DOMContentLoaded", initSlider);
63    } else {
64      initSlider();
65    }
66  }
67
68  tryInit();
69
70  const observer = new MutationObserver(() => {
71    initSlider();
72  });
73
74  observer.observe(document.body, {
75    childList: true,
76    subtree: true,
77  });
78})();
79
80(function () {
81  function initCatalogFilterCount() {
82    const form = document.querySelector(".catalog-filters-items");
83    if (!form || form.dataset.countInit) return false;
84
85    const categoryInputs = Array.from(
86      form.querySelectorAll('input[name="categories"]'),
87    );
88    const resultCount = form.querySelector("[data-filter-result-count]");
89    if (!categoryInputs.length || !resultCount) return false;
90
91    const allResultCount = Number(form.dataset.allResultCount) || 0;
92    const numberFormatter = new Intl.NumberFormat("ru-RU");
93
94    function updateResultCount() {
95      const selectedInputs = categoryInputs.filter((input) => input.checked);
96      const selectedTotal = selectedInputs.reduce(
97        (total, input) => total + (Number(input.dataset.resultCount) || 0),
98        0,
99      );
100
101      resultCount.textContent = numberFormatter.format(
102        selectedInputs.length ? selectedTotal : allResultCount,
103      );
104    }
105
106    categoryInputs.forEach((input) =>
107      input.addEventListener("change", updateResultCount),
108    );
109    form.dataset.countInit = "true";
110    updateResultCount();
111    return true;
112  }
113
114  if (document.readyState === "loading") {
115    document.addEventListener("DOMContentLoaded", initCatalogFilterCount);
116  } else {
117    initCatalogFilterCount();
118  }
119})();
120
121(function () {
122  function initFilterCategory() {
123    const scrollBox = document.getElementById("filterScroll");
124    const nextBtn = document.getElementById("filterNextBtn");
125
126    if (!scrollBox || !nextBtn) return false;
127    if (scrollBox.dataset.filterInit) return true;
128    scrollBox.dataset.filterInit = "true";
129
130    const scrollStep = 200;
131
132    function updateNextButton() {
133      const maxScroll = scrollBox.scrollWidth - scrollBox.clientWidth;
134      nextBtn.classList.toggle(
135        "is-disabled",
136        scrollBox.scrollLeft >= maxScroll - 2,
137      );
138    }
139
140    nextBtn.addEventListener("click", () => {
141      scrollBox.scrollBy({ left: scrollStep, behavior: "smooth" });
142    });
143
144    scrollBox.addEventListener("scroll", updateNextButton);
145    window.addEventListener("resize", updateNextButton);
146    updateNextButton();
147
148    // --- drag-to-scroll мышью ---
149    let isDragging = false;
150    let startX = 0;
151    let startScrollLeft = 0;
152    let hasMoved = false;
153
154    scrollBox.addEventListener("mousedown", (e) => {
155      isDragging = true;
156      hasMoved = false;
157      startX = e.pageX;
158      startScrollLeft = scrollBox.scrollLeft;
159      scrollBox.classList.add("is-dragging");
160    });
161
162    window.addEventListener("mousemove", (e) => {
163      if (!isDragging) return;
164      e.preventDefault();
165
166      const delta = e.pageX - startX;
167
168      // если сдвинули мышь больше чем на 3px — считаем это драгом, а не кликом
169      if (Math.abs(delta) > 3) hasMoved = true;
170
171      scrollBox.scrollLeft = startScrollLeft - delta;
172    });
173
174    window.addEventListener("mouseup", () => {
175      if (!isDragging) return;
176      isDragging = false;
177      scrollBox.classList.remove("is-dragging");
178    });
179
180    // блокируем клик по кнопке category, если это был драг, а не клик
181    scrollBox.addEventListener(
182      "click",
183      (e) => {
184        if (hasMoved) {
185          e.preventDefault();
186          e.stopPropagation();
187        }
188      },
189      true,
190    );
191
192    return true;
193  }
194
195  if (document.readyState === "loading") {
196    document.addEventListener("DOMContentLoaded", initFilterCategory);
197  } else {
198    initFilterCategory();
199  }
200
201  const observer = new MutationObserver(() => initFilterCategory());
202  observer.observe(document.body, { childList: true, subtree: true });
203})();
204
205(function () {
206  const isCatalog = /^\/presentations\/catalog(?:\/|$)/.test(
207    window.location.pathname,
208  );
209
210  if (!isCatalog) {
211    document.body.classList.remove("body-fon");
212    return;
213  }
214
215  const panel = document.querySelector(
216    ".catalog-main-section .catalog-filters-main",
217  );
218  const button = document.querySelector(".open-estate-filter");
219  const handle = panel?.querySelector(".modal-drag-zone");
220  if (!panel || !button || !handle) return;
221
222  const mobile = window.matchMedia("(max-width: 1020px)");
223  const storageKey = "catalogFiltersOpen";
224  let isOpen = false;
225  let drag = null;
226
227  try {
228    isOpen = sessionStorage.getItem(storageKey) === "true";
229  } catch (_error) {
230    // The filter still works when browser storage is unavailable.
231  }
232
233  handle.tabIndex = 0;
234  handle.setAttribute("role", "button");
235
236  function setOpen(value, save = true) {
237    isOpen = value;
238    panel.classList.toggle("touchstart-open", value);
239    button.classList.toggle("active", value);
240    document.body.classList.toggle("body-fon", value);
241    handle.setAttribute("aria-expanded", String(value));
242    handle.setAttribute("aria-label", value ? "Закрыть фильтры" : "Открыть фильтры");
243    button.setAttribute("aria-expanded", String(value));
244
245    for (const child of handle.parentElement.children) {
246      if (child !== handle) child.inert = !value;
247    }
248
249    if (save) {
250      try {
251        sessionStorage.setItem(storageKey, String(value));
252      } catch (_error) {
253        // Opening and closing does not require storage access.
254      }
255    }
256  }
257
258  button.addEventListener("click", () => setOpen(!isOpen));
259  handle.addEventListener("keydown", (event) => {
260    if (event.key !== "Enter" && event.key !== " ") return;
261    event.preventDefault();
262    setOpen(!isOpen);
263  });
264
265  handle.addEventListener("pointerdown", (event) => {
266    if (!mobile.matches || !event.isPrimary || event.button !== 0) return;
267    const distance = Math.max(0, panel.offsetHeight - handle.offsetHeight);
268    drag = {
269      id: event.pointerId,
270      startY: event.clientY,
271      distance,
272      initial: isOpen ? 0 : distance,
273      delta: 0,
274      wasOpen: isOpen,
275    };
276    handle.setPointerCapture(event.pointerId);
277    panel.classList.add("is-dragging");
278  });
279
280  handle.addEventListener("pointermove", (event) => {
281    if (!drag || event.pointerId !== drag.id) return;
282    drag.delta = event.clientY - drag.startY;
283    const offset = Math.max(0, Math.min(drag.distance, drag.initial + drag.delta));
284    panel.style.transform = `translate(-50%, ${offset}px)`;
285  });
286
287  function finishDrag(event, cancelled = false) {
288    if (!drag || event.pointerId !== drag.id) return;
289    const current = drag;
290    drag = null;
291    const nextOpen = cancelled
292      ? current.wasOpen
293      : current.wasOpen
294        ? current.delta < 50
295        : current.delta < -50;
296
297    panel.classList.remove("is-dragging");
298    setOpen(nextOpen, !cancelled);
299    panel.style.removeProperty("transform");
300    if (handle.hasPointerCapture(current.id)) {
301      handle.releasePointerCapture(current.id);
302    }
303  }
304
305  handle.addEventListener("pointerup", (event) => finishDrag(event));
306  handle.addEventListener("pointercancel", (event) => finishDrag(event, true));
307  handle.addEventListener("lostpointercapture", (event) => finishDrag(event, true));
308
309  document.addEventListener("click", (event) => {
310    if (!mobile.matches || !isOpen) return;
311    if (panel.contains(event.target) || button.contains(event.target)) return;
312    setOpen(false);
313  });
314
315  mobile.addEventListener("change", () => {
316    if (drag) finishDrag({ pointerId: drag.id }, true);
317  });
318
319  setOpen(isOpen, false);
320})();
321
322(
323  // The catalog template uses <a> elements inside the custom sort options,
324  // while the legacy global handler expects <span>. Keep sort handling local
325  // to the catalog: sync the visual state from the URL and make the whole row
326  // reliably navigate without triggering the legacy handler's null access.
327  function () {
328    function initCatalogSort() {
329      const sortSelect = document.querySelector(
330        ".sort-select .c-categories-select",
331      );
332      if (!sortSelect || sortSelect.dataset.sortInit) return false;
333
334      const headerText = sortSelect.querySelector(".c-select-header p");
335      const optionsList = sortSelect.querySelector(".c-select-options");
336      const options = Array.from(
337        sortSelect.querySelectorAll(".c-select-options li"),
338      );
339      if (!headerText || !optionsList || !options.length) return false;
340
341      const requestedSort = new URLSearchParams(window.location.search).get(
342        "sort",
343      );
344      const currentSort = requestedSort === "newest" ? "newest" : "popular";
345
346      function getOptionData(option) {
347        const link = option.querySelector("a[href]");
348        if (!link) return null;
349
350        const url = new URL(link.href, window.location.href);
351        const sort =
352          url.searchParams.get("sort") === "newest" ? "newest" : "popular";
353        const label = link.textContent.trim();
354        return { link, sort, label };
355      }
356
357      let activeOption = null;
358
359      options.forEach((option) => {
360        const data = getOptionData(option);
361        const isActive = Boolean(data && data.sort === currentSort);
362        option.classList.toggle("active", isActive);
363        if (isActive) activeOption = data;
364      });
365
366      if (activeOption) {
367        headerText.textContent = activeOption.label;
368      }
369
370      optionsList.addEventListener(
371        "click",
372        (event) => {
373          const option = event.target.closest("li");
374          if (!option || !optionsList.contains(option)) return;
375
376          const data = getOptionData(option);
377          if (!data) return;
378
379          // Stop the legacy index.js LI handler before it tries
380          // querySelector('span').textContent on this anchor-based markup.
381          event.preventDefault();
382          event.stopPropagation();
383
384          window.location.assign(data.link.href);
385        },
386        true,
387      );
388
389      sortSelect.dataset.sortInit = "true";
390      return true;
391    }
392
393    if (document.readyState === "loading") {
394      document.addEventListener("DOMContentLoaded", initCatalogSort);
395    } else {
396      initCatalogSort();
397    }
398  }
399)();

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.