PageSourceSearch

https://subra.bg/frontend/js/custom-scripts.js?1791156015

js subra.bg collected 2026-10-04 23:20:44 UTC 31,756 bytes, 967 lines download raw bytes

1/* Custom site scripts. Add page/feature-specific behaviour here. */
2(function () {
3  "use strict";
4
5  // ── "За аптеки Subra" — storytelling timeline (scroll reveal + arrow images) ──
6  function initStorytellingTimeline() {
7    var hasItems = document.querySelector(".tl-item, .st-values-title, .st-value-card");
8    if (!hasItems) return;
9
10    // Swap the timeline arrow image per theme.
11    var themes = [
12      { selector: ".tl-theme--orange", arrow: "/frontend/images-new/arrow_green.png" },
13      { selector: ".tl-theme--blue", arrow: "/frontend/images-new/arrow_blue.png" },
14      { selector: ".tl-theme--green", arrow: "/frontend/images-new/arrow_green2.jpg" }
15    ];
16    themes.forEach(function (theme) {
17      Array.prototype.forEach.call(document.querySelectorAll(theme.selector), function (el) {
18        var arrow = el.querySelector(".tl-arrow-img");
19        if (arrow) arrow.src = theme.arrow;
20      });
21    });
22
23    // Reveal items on scroll, one after another in DOM order. Each item appears
24    // only after the previous one in its group has appeared AND it has scrolled
25    // into view. Each group (timeline / value cards) sequences independently.
26    var STAGGER = 450;
27    var timeline = document.querySelector(".st-timeline");
28    var timelineItems = Array.prototype.slice.call(document.querySelectorAll(".tl-item"));
29    var groups = [
30      timelineItems,
31      Array.prototype.slice.call(document.querySelectorAll(".st-values-title, .st-value-card"))
32    ];
33
34    groups.forEach(function (items) {
35      if (!items.length) return;
36      var isTimelineGroup = timeline && items === timelineItems;
37
38      if (!("IntersectionObserver" in window)) {
39        items.forEach(function (el) { el.classList.add("is-visible"); });
40        if (isTimelineGroup) timeline.classList.add("is-visible");
41        return;
42      }
43
44      var ready = [];   // ready[i] = true once item i has scrolled into view
45      var next = 0;     // index of the next item to reveal
46      var animating = false;
47
48      function flush() {
49        if (animating || next >= items.length || !ready[next]) return;
50        animating = true;
51        items[next].classList.add("is-visible");
52        if (isTimelineGroup && next === 0) {
53          window.requestAnimationFrame(function () {
54            timeline.classList.add("is-visible");
55          });
56        }
57        next++;
58        setTimeout(function () {
59          animating = false;
60          flush();
61        }, STAGGER);
62      }
63
64      var io = new IntersectionObserver(function (entries) {
65        entries.forEach(function (entry) {
66          if (entry.isIntersecting) {
67            var idx = items.indexOf(entry.target);
68            if (idx > -1) ready[idx] = true;
69            io.unobserve(entry.target);
70          }
71        });
72        flush();
73      }, { threshold: 0.15 });
74
75      items.forEach(function (el) { io.observe(el); });
76    });
77  }
78
79  // ── Offices / pharmacy addresses — "show all" slide-down toggle (no Bootstrap JS) ──
80  function initOfficesToggle() {
81    var toggles = document.querySelectorAll(".js-offices-toggle");
82    if (!toggles.length) return;
83
84    Array.prototype.forEach.call(toggles, function (btn) {
85      var more = document.getElementById(btn.getAttribute("aria-controls"));
86      if (!more) return;
87
88      var labelEl = btn.querySelector(".js-offices-toggle-text");
89      var showText = (labelEl && labelEl.textContent) || "Покажи всички";
90      var hideText = btn.getAttribute("data-hide-text") || "Покажи по-малко";
91
92      btn.addEventListener("click", function () {
93        var isOpen = more.classList.toggle("is-open");
94        btn.classList.toggle("is-open", isOpen);
95        btn.setAttribute("aria-expanded", isOpen ? "true" : "false");
96        more.style.maxHeight = isOpen ? more.scrollHeight + "px" : "0px";
97        if (labelEl) labelEl.textContent = isOpen ? hideText : showText;
98      });
99
100      // Keep the open height correct when the layout reflows.
101      window.addEventListener("resize", function () {
102        if (more.classList.contains("is-open")) {
103          more.style.maxHeight = more.scrollHeight + "px";
104        }
105      });
106    });
107  }
108
109  // ── Footer navigation — collapsible categories on mobile only (no Bootstrap JS) ──
110  function initFooterAccordion() {
111    var cols = document.querySelectorAll(".main-footer .footer-nav__col");
112    if (!cols.length) return;
113
114    var mq = window.matchMedia("(max-width: 767.98px)");
115
116    var panels = [];
117    Array.prototype.forEach.call(cols, function (col) {
118      var title = col.querySelector(".footer-nav__title");
119      var list = col.querySelector(".footer-nav__list");
120      if (!title || !list) return;
121      panels.push({ col: col, title: title, list: list });
122
123      function toggle() {
124        if (!mq.matches) return;
125        var isOpen = col.classList.toggle("is-open");
126        list.style.maxHeight = isOpen ? list.scrollHeight + "px" : "0px";
127        title.setAttribute("aria-expanded", isOpen ? "true" : "false");
128      }
129
130      title.addEventListener("click", toggle);
131      title.addEventListener("keydown", function (e) {
132        if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") {
133          e.preventDefault();
134          toggle();
135        }
136      });
137    });
138
139    // Apply/remove the interactive state to match the current viewport, so the
140    // desktop DOM stays untouched (no inline styles, no aria/role attributes).
141    function sync() {
142      panels.forEach(function (p) {
143        if (mq.matches) {
144          p.title.setAttribute("role", "button");
145          p.title.setAttribute("tabindex", "0");
146          if (!p.col.classList.contains("is-open")) {
147            p.title.setAttribute("aria-expanded", "false");
148          } else {
149            p.list.style.maxHeight = p.list.scrollHeight + "px";
150          }
151        } else {
152          p.col.classList.remove("is-open");
153          p.list.style.maxHeight = "";
154          p.title.removeAttribute("role");
155          p.title.removeAttribute("tabindex");
156          p.title.removeAttribute("aria-expanded");
157        }
158      });
159    }
160
161    sync();
162    if (mq.addEventListener) mq.addEventListener("change", sync);
163    else if (mq.addListener) mq.addListener(sync);
164
165    window.addEventListener("resize", function () {
166      if (!mq.matches) return;
167      panels.forEach(function (p) {
168        if (p.col.classList.contains("is-open")) {
169          p.list.style.maxHeight = p.list.scrollHeight + "px";
170        }
171      });
172    });
173  }
174
175  function initTopbarHide() {
176    var header = document.querySelector('.site-header');
177    if (!header) return;
178    var lastY = 0;
179    var locked = false;
180
181    function getScrollY() {
182      return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
183    }
184
185    document.addEventListener('scroll', function () {
186      if (locked) return;
187      var y = getScrollY();
188      if (y > lastY && y > 60) {
189        header.classList.add('header--compact');
190        locked = true;
191        setTimeout(function () { locked = false; lastY = getScrollY(); }, 350);
192      } else if (y < lastY) {
193        header.classList.remove('header--compact');
194        locked = true;
195        setTimeout(function () { locked = false; lastY = getScrollY(); }, 350);
196      }
197      lastY = y;
198    }, { passive: true, capture: true });
199  }
200
201  function initMobileFloatingSearch() {
202    var header = document.querySelector(".site-header");
203    var search = document.querySelector(".site-header .search");
204    var floatButton = document.querySelector(".mobile-search-float");
205    if (!header) return;
206    if (!search) return;
207    if (!floatButton) return;
208
209    var input = search.querySelector(".search__input");
210    if (!input) return;
211
212    var mq = window.matchMedia("(max-width: 768px)");
213    var lastY = window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
214    var ticking = false;
215
216    function getScrollY() {
217      return window.scrollY || document.documentElement.scrollTop || document.body.scrollTop || 0;
218    }
219
220    function focusSearchAtTop() {
221      header.classList.add("mobile-search-focus");
222      floatButton.classList.remove("is-visible");
223      floatButton.setAttribute("aria-expanded", "true");
224
225      window.scrollTo({ top: 0, behavior: "smooth" });
226      window.setTimeout(function () {
227        input.focus();
228        floatButton.setAttribute("aria-expanded", "false");
229      }, 550);
230    }
231
232    input.addEventListener("blur", function () {
233      window.setTimeout(function () {
234        header.classList.remove("mobile-search-focus");
235      }, 160);
236    });
237
238    floatButton.setAttribute("aria-expanded", "false");
239
240    floatButton.addEventListener(window.PointerEvent ? "pointerup" : "click", function (event) {
241      if (!mq.matches) return;
242      event.preventDefault();
243      focusSearchAtTop();
244    });
245
246    floatButton.addEventListener("keyup", function (event) {
247      if (event.key !== "Enter" && event.key !== " " && event.key !== "Spacebar") return;
248      if (!mq.matches) return;
249      event.preventDefault();
250      focusSearchAtTop();
251    });
252
253    function syncFloatButton() {
254      if (!mq.matches) return;
255
256      var y = getScrollY();
257      var delta = y - lastY;
258
259      if (y <= 120) {
260        floatButton.classList.remove("is-visible");
261      } else if (delta > 8 && y > 160) {
262        floatButton.classList.add("is-visible");
263      } else if (delta < -40) {
264        floatButton.classList.remove("is-visible");
265      }
266
267      lastY = y;
268    }
269
270    function requestSync() {
271      if (ticking) return;
272      ticking = true;
273      window.requestAnimationFrame(function () {
274        ticking = false;
275        syncFloatButton();
276      });
277    }
278
279    window.addEventListener("scroll", requestSync, { passive: true });
280    document.addEventListener("scroll", requestSync, { passive: true, capture: true });
281
282    function sync() {
283      if (!mq.matches) {
284        header.classList.remove("mobile-search-focus");
285        floatButton.classList.remove("is-visible");
286      } else if (getScrollY() <= 80) {
287        floatButton.classList.remove("is-visible");
288      }
289    }
290
291    sync();
292    if (mq.addEventListener) mq.addEventListener("change", sync);
293    else if (mq.addListener) mq.addListener(sync);
294
295    window.setInterval(syncFloatButton, 250);
296  }
297
298
299  function initProfileEditFormScroll() {
300    var buttons = document.querySelectorAll(".profile-page .edit-icon.js-show-form-btn.js-hide-form-btn");
301    if (!buttons.length) return;
302
303    Array.prototype.forEach.call(buttons, function (button) {
304      if (button.classList.contains("profile-edit-scroll-inited")) return;
305      button.classList.add("profile-edit-scroll-inited");
306
307      button.addEventListener("click", function () {
308        var section = button.closest(".js-adress");
309        if (!section) return;
310
311        var formWrapper = section.querySelector(".js-show-form");
312        if (!formWrapper) return;
313
314        setTimeout(function () {
315          if (formWrapper.classList.contains("hidden")) return;
316
317          var headerOffset = window.matchMedia("(max-width: 767.98px)").matches ? 90 : 130;
318          var top = formWrapper.getBoundingClientRect().top + window.pageYOffset - headerOffset;
319          window.scrollTo({ top: top, behavior: "smooth" });
320        }, 80);
321      });
322    });
323  }
324
325  function getLocationsScrollOffset() {
326    var desktopOffset = 130;
327    var mobileOffset = 18;
328    var header = document.querySelector(".main-header");
329
330    if (window.innerWidth < 992) return mobileOffset;
331
332    if (header) {
333      var headerHeight = header.getBoundingClientRect().height;
334      if (headerHeight > 0) return Math.ceil(headerHeight + 18);
335    }
336
337    return desktopOffset;
338  }
339
340  function scrollLocationsWithHeaderOffset(locations) {
341    if (!locations) return;
342
343    var top = locations.getBoundingClientRect().top + window.pageYOffset - getLocationsScrollOffset();
344    window.scrollTo({
345      top: Math.max(0, top),
346      behavior: "smooth"
347    });
348  }
349
350  function initNearestPharmacy() {
351    var locations = document.querySelector(".js-locations[data-id]");
352    if (!locations) return;
353
354    var nearestButtons = locations.querySelectorAll(".js-nearest-pharmacy");
355    if (!nearestButtons.length) return;
356
357    function setNearestButtonsLoading(isLoading) {
358      Array.prototype.forEach.call(nearestButtons, function (button) {
359        button.classList.toggle("is-locating", isLoading);
360        button.disabled = isLoading;
361      });
362    }
363
364    function showError(message) {
365      if (window.flashMsg && typeof window.flashMsg.showError === "function") {
366        window.flashMsg.showError(message);
367        return;
368      }
369
370      if (window.StudioX && window.StudioX.toastr && typeof window.StudioX.toastr.error === "function") {
371        window.StudioX.toastr.error(message);
372        return;
373      }
374
375      alert(message);
376    }
377
378    function getInstance() {
379      if (!window.StudioX || !window.StudioX.LocationsInstances) return null;
380      return window.StudioX.LocationsInstances[locations.dataset.id] || null;
381    }
382
383    function waitForInstance(callback, attempt) {
384      var instance = getInstance();
385      var isReady = instance && instance.locationMap && instance.markers && instance.markers.length;
386
387      if (isReady) {
388        callback(instance);
389        return;
390      }
391
392      if ((attempt || 0) >= 50) {
393        showError("Картата все още не е заредена. Моля, опитайте отново.");
394        setNearestButtonsLoading(false);
395        return;
396      }
397
398      window.setTimeout(function () {
399        waitForInstance(callback, (attempt || 0) + 1);
400      }, 100);
401    }
402
403    function toRad(value) {
404      return value * Math.PI / 180;
405    }
406
407    function distanceBetween(lat1, lng1, lat2, lng2) {
408      var earthRadius = 6371;
409      var dLat = toRad(lat2 - lat1);
410      var dLng = toRad(lng2 - lng1);
411      var a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
412        Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
413        Math.sin(dLng / 2) * Math.sin(dLng / 2);
414      var c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
415      return earthRadius * c;
416    }
417
418    function findClosestLocation(locationsData, userLat, userLng) {
419      var closest = null;
420      var closestDistance = Infinity;
421
422      locationsData.forEach(function (location) {
423        var lat = parseFloat(location.latitude);
424        var lng = parseFloat(location.longitude);
425
426        if (isNaN(lat) || isNaN(lng)) return;
427
428        var distance = distanceBetween(userLat, userLng, lat, lng);
429        if (distance < closestDistance) {
430          closest = location;
431          closestDistance = distance;
432        }
433      });
434
435      return closest;
436    }
437
438    function getButtonsById(id) {
439      var buttons = locations.querySelectorAll(".js-location-button");
440      var result = [];
441
442      Array.prototype.forEach.call(buttons, function (button) {
443        if (String(button.dataset.id) === String(id)) result.push(button);
444      });
445
446      return result;
447    }
448
449    function clearNearestState() {
450      Array.prototype.forEach.call(locations.querySelectorAll(".is-nearest"), function (element) {
451        element.classList.remove("is-nearest");
452      });
453    }
454
455    function markNearestLocation(id) {
456      var buttons = getButtonsById(id);
457      var sidebarButton = null;
458
459      clearNearestState();
460
461      buttons.forEach(function (button) {
462        var sidebarItem = button.closest(".locations-sidebar-item");
463        var listItem = button.closest(".locations-list__item");
464
465        if (sidebarItem) {
466          sidebarItem.classList.add("is-nearest");
467          sidebarButton = button;
468        }
469
470        if (listItem) listItem.classList.add("is-nearest");
471      });
472
473      if (sidebarButton) {
474        sidebarButton.scrollIntoView({ behavior: "smooth", block: "nearest" });
475      }
476    }
477
478    function resetCityFilter(instance) {
479      if (typeof instance.filterData === "function") {
480        instance.checkedFilterType = "all";
481        instance.filterData("all", instance.locationsData);
482      }
483
484      if (instance.filterTypeElements) {
485        instance.filterTypeElements.forEach(function (filter) {
486          filter.checked = filter.value === "all";
487        });
488      }
489
490      if (instance.filterTypeSelect) {
491        var hasAllOption = Array.prototype.some.call(instance.filterTypeSelect.options, function (option) {
492          return option.value === "all";
493        });
494        if (hasAllOption) instance.filterTypeSelect.value = "all";
495      }
496    }
497
498    function openClosestLocation(position) {
499      waitForInstance(function (instance) {
500        var closest = findClosestLocation(
501          instance.locationsData || [],
502          position.coords.latitude,
503          position.coords.longitude
504        );
505
506        if (!closest) {
507          showError("Не успяхме да намерим аптека с координати.");
508          setNearestButtonsLoading(false);
509          return;
510        }
511
512        resetCityFilter(instance);
513        instance.buildInfoWindow(closest.id);
514        markNearestLocation(closest.id);
515        scrollLocationsWithHeaderOffset(locations);
516
517        setNearestButtonsLoading(false);
518      });
519    }
520
521    Array.prototype.forEach.call(nearestButtons, function (nearestButton) {
522      nearestButton.addEventListener("click", function () {
523      if (!navigator.geolocation) {
524        showError("Вашият браузър не поддържа споделяне на локация.");
525        return;
526      }
527
528      setNearestButtonsLoading(true);
529
530      navigator.geolocation.getCurrentPosition(openClosestLocation, function () {
531        showError("Не получихме достъп до локацията. Моля, разрешете достъп от браузъра и опитайте отново.");
532        setNearestButtonsLoading(false);
533      }, {
534        enableHighAccuracy: true,
535        timeout: 10000,
536        maximumAge: 60000
537      });
538      });
539    });
540  }
541
542  function initLocationsScrollOffset() {
543    var locations = document.querySelector(".js-locations[data-id]");
544    if (!locations) return;
545
546    Array.prototype.forEach.call(locations.querySelectorAll(".js-location-button"), function (button) {
547      button.addEventListener("click", function () {
548        window.setTimeout(function () {
549          scrollLocationsWithHeaderOffset(locations);
550        }, 80);
551      });
552    });
553  }
554
555  // ── Product gallery — prev/next arrows ──
556  // The "product" slider preset in main.js configures pagination only, so the
557  // arrows are wired to Swiper's own navigation module here. That keeps the
558  // swiper-button-disabled state handling for free. Swiper is initialised by
559  // main.js on its own schedule, so poll briefly for the instance.
560  function initProductGalleryNav() {
561    var gallery = document.querySelector(".product-block_backend .product__gallery");
562    if (!gallery) return;
563
564    var prev = gallery.querySelector(".js-slider-prev");
565    var next = gallery.querySelector(".js-slider-next");
566    if (!prev || !next) return;
567
568    var tries = 0;
569    (function wire() {
570      var swiper = gallery.swiper;
571      if (!swiper || !swiper.navigation) {
572        if (++tries > 60) return;
573        window.requestAnimationFrame(wire);
574        return;
575      }
576      swiper.params.navigation.prevEl = prev;
577      swiper.params.navigation.nextEl = next;
578      swiper.navigation.init();
579      swiper.navigation.update();
580    })();
581  }
582
583  // ── Product tabs — accordion behaviour on mobile ──
584  // Tabby always keeps one tab selected. The mobile accordion starts fully
585  // collapsed and lets you close the open row again, so undo Tabby's default
586  // selection and handle the "click the open row" case ourselves.
587  function initProductTabsAccordion() {
588    var tabs = document.querySelector(".product-block_backend .tabs");
589    if (!tabs) return;
590
591    var links = Array.prototype.slice.call(tabs.querySelectorAll(".tabs__nav-link"));
592    if (!links.length) return;
593
594    var mq = window.matchMedia("(max-width: 768px)");
595
596    function panelOf(link) {
597      return document.querySelector(link.getAttribute("href"));
598    }
599    function close(link) {
600      link.setAttribute("aria-selected", "false");
601      var panel = panelOf(link);
602      if (panel) panel.hidden = true;
603    }
604    function collapseAll() {
605      links.forEach(close);
606    }
607    function openDefault() {
608      var isOpen = links.some(function (link) {
609        return link.getAttribute("aria-selected") === "true";
610      });
611      if (isOpen) return;
612      links[0].setAttribute("aria-selected", "true");
613      var panel = panelOf(links[0]);
614      if (panel) panel.hidden = false;
615    }
616
617    // Panels transition on `height`, which needs pixel values at both ends —
618    // "auto" cannot be animated. Freeze the height, flip to the target, then
619    // hand the height back to CSS once the transition is done.
620    // keep in sync with the .tabs__body transition in styles.css
621    var DURATION = 1000;
622
623    // Open height of a panel, whether it is currently open or not. Measured with
624    // the transition off so an in-flight padding animation cannot skew it.
625    function measure(panel) {
626      var height = panel.style.height;
627      var transition = panel.style.transition;
628      var wasHidden = panel.hidden;
629
630      panel.style.transition = "none";
631      panel.hidden = false;
632      panel.style.height = "auto";
633      var open = panel.offsetHeight;
634
635      panel.style.height = height;
636      panel.hidden = wasHidden;
637      panel.offsetHeight; // flush, so restoring the state is not animated
638      panel.style.transition = transition;
639
640      return open;
641    }
642    function settle(panel) {
643      window.setTimeout(function () {
644        panel.style.height = "";
645      }, DURATION);
646    }
647    function slideShut(panel) {
648      panel.style.height = "0px";
649      settle(panel);
650    }
651
652    // Tabby initialises on DOMContentLoaded too, so wait a frame for it.
653    window.requestAnimationFrame(function () {
654      if (mq.matches) collapseAll();
655    });
656
657    // Tabby captures clicks on documentElement, so by the time a listener on the
658    // link itself runs it has already flipped aria-selected and the hidden
659    // attributes. Capture on document — one level above — to pin the current
660    // heights first, so the panels have somewhere to animate from.
661    document.addEventListener("click", function (event) {
662      if (!mq.matches || !event.target.closest) return;
663
664      var link = event.target.closest(".product-block_backend .tabs__nav-link");
665      if (!link || links.indexOf(link) === -1) return;
666
667      var panel = panelOf(link);
668      if (!panel) return;
669
670      var wasOpen = link.getAttribute("aria-selected") === "true";
671      var openPanels = links
672        .filter(function (other) {
673          return other !== link && other.getAttribute("aria-selected") === "true";
674        })
675        .map(panelOf);
676
677      if (wasOpen) {
678        // Tabby ignores a click on the selected tab, so this is ours to close.
679        panel.style.height = measure(panel) + "px";
680        panel.offsetHeight; // commit the start height, or there is nothing to animate from
681        window.requestAnimationFrame(function () {
682          close(link);
683          slideShut(panel);
684        });
685        return;
686      }
687
688      var target = measure(panel);
689      panel.style.height = "0px";
690      openPanels.forEach(function (other) {
691        if (other) other.style.height = measure(other) + "px";
692      });
693      panel.offsetHeight; // commit the start heights before Tabby flips the panels
694
695      window.requestAnimationFrame(function () {
696        panel.style.height = target + "px";
697        settle(panel);
698        openPanels.forEach(function (other) {
699          if (other) slideShut(other);
700        });
701      });
702    }, true);
703
704    if (mq.addEventListener) {
705      mq.addEventListener("change", function (event) {
706        if (event.matches) collapseAll();
707        else openDefault();
708      });
709    }
710  }
711
712  function initMegaMenuProducts() {
713    var grid = document.querySelector("#mega-menu .mega__grid");
714    var categories = document.querySelector("#mega-menu [data-js='mega-cats']");
715    if (!grid || !categories) return;
716
717    var mobileMenu = window.matchMedia("(max-width: 1199.98px)");
718    var panels = Array.prototype.slice.call(
719      grid.querySelectorAll("[data-mega-products]")
720    );
721    var activeCategory = null;
722
723    panels.forEach(function (panel) {
724      var marker = document.createComment("mega-products-position");
725      panel.parentNode.insertBefore(marker, panel);
726      panel._megaProductsMarker = marker;
727    });
728
729    function restorePanels() {
730      panels.forEach(function (panel) {
731        var marker = panel._megaProductsMarker;
732        if (marker && marker.parentNode && panel.parentNode !== marker.parentNode) {
733          marker.parentNode.insertBefore(panel, marker.nextSibling);
734        }
735      });
736    }
737
738    function showProducts(category) {
739      if (!category) return;
740
741      activeCategory = category;
742      restorePanels();
743
744      var panelId = category.getAttribute("data-cat");
745      var activePanel = null;
746
747      panels.forEach(function (panel) {
748        var isActive = panel.getAttribute("data-mega-products") === panelId;
749        panel.hidden = !isActive;
750        if (isActive) activePanel = panel;
751      });
752
753      if (mobileMenu.matches && activePanel) {
754        var accordionPanel = category.nextElementSibling;
755        if (accordionPanel && accordionPanel.classList.contains("mega-cat__panel")) {
756          accordionPanel.insertBefore(activePanel, accordionPanel.firstChild);
757        }
758      }
759
760      grid.classList.toggle("mega__grid--without-products", !activePanel);
761    }
762
763    categories.addEventListener("mouseover", function (event) {
764      var category = event.target.closest && event.target.closest(".mega-cat");
765      if (category) showProducts(category);
766    });
767
768    categories.addEventListener("click", function (event) {
769      var category = event.target.closest && event.target.closest(".mega-cat");
770      if (category) showProducts(category);
771    });
772
773    function syncProductsPosition() {
774      var category = mobileMenu.matches
775        ? categories.querySelector('.mega-cat[aria-expanded="true"]')
776        : categories.querySelector(".mega-cat.is-active");
777
778      showProducts(category || activeCategory || categories.querySelector(".mega-cat"));
779    }
780
781    if (typeof mobileMenu.addEventListener === "function") {
782      mobileMenu.addEventListener("change", syncProductsPosition);
783    } else {
784      mobileMenu.addListener(syncProductsPosition);
785    }
786
787    showProducts(categories.querySelector(".mega-cat.is-active, .mega-cat"));
788  }
789
790  function initProductDescriptionClamp() {
791    var panel = document.querySelector(".product-block_backend #product_tabs_1");
792    if (!panel) return;
793
794    var text = panel.querySelector(".js-product-description");
795    var toggle = panel.querySelector(".js-product-description-toggle");
796    if (!text || !toggle) return;
797
798    var label = toggle.querySelector(".product-description__toggle-text");
799    var mq = window.matchMedia("(min-width: 769px)");
800    var CLAMP = 338;
801    var DURATION = 600;
802    var cut = CLAMP;
803    var settleTimer = null;
804
805    function lastLineBottom() {
806      var lineHeight = parseFloat(window.getComputedStyle(text).lineHeight);
807      var top = text.getBoundingClientRect().top;
808      var walker = document.createTreeWalker(text, NodeFilter.SHOW_TEXT, null, false);
809      var range = document.createRange();
810      var best = 0;
811      var node;
812
813      while ((node = walker.nextNode())) {
814        if (!node.nodeValue.replace(/\s/g, "")) continue;
815
816        range.selectNodeContents(node);
817        var rects = range.getClientRects();
818
819        for (var i = 0; i < rects.length; i++) {
820          var rect = rects[i];
821          var leading = lineHeight > rect.height ? (lineHeight - rect.height) / 2 : 0;
822          var bottom = rect.bottom + leading - top;
823
824          if (bottom <= CLAMP && bottom > best) best = bottom;
825        }
826      }
827
828      return best;
829    }
830
831    function collapse(animate) {
832      window.clearTimeout(settleTimer);
833      toggle.setAttribute("aria-expanded", "false");
834      if (label) label.textContent = toggle.getAttribute("data-more");
835
836      if (!animate) {
837        text.classList.add("is-clamped");
838        text.style.maxHeight = cut + "px";
839        return;
840      }
841
842      text.style.maxHeight = text.scrollHeight + "px";
843      text.classList.add("is-clamped");
844      text.offsetHeight;
845
846      window.requestAnimationFrame(function () {
847        text.style.maxHeight = cut + "px";
848      });
849    }
850    function expand() {
851      window.clearTimeout(settleTimer);
852      toggle.setAttribute("aria-expanded", "true");
853      if (label) label.textContent = toggle.getAttribute("data-less");
854
855      text.style.maxHeight = text.scrollHeight + "px";
856
857      settleTimer = window.setTimeout(function () {
858        text.classList.remove("is-clamped");
859        text.style.maxHeight = "";
860      }, DURATION);
861    }
862
863    function refresh() {
864      var wasCollapsed = toggle.getAttribute("aria-expanded") === "false";
865
866      window.clearTimeout(settleTimer);
867      text.classList.remove("is-clamped");
868      text.style.maxHeight = "";
869
870      if (!mq.matches) {
871        toggle.classList.remove("is-visible");
872        return;
873      }
874
875      var overflows = text.scrollHeight > CLAMP;
876      toggle.classList.toggle("is-visible", overflows);
877
878      if (!overflows) return;
879
880      cut = lastLineBottom() || CLAMP;
881      if (wasCollapsed) collapse(false);
882    }
883
884    toggle.addEventListener("click", function () {
885      if (toggle.getAttribute("aria-expanded") === "true") collapse(true);
886      else expand();
887    });
888
889    var resizeTimer = null;
890    window.addEventListener("resize", function () {
891      window.clearTimeout(resizeTimer);
892      resizeTimer = window.setTimeout(refresh, 150);
893    });
894
895    refresh();
896    window.addEventListener("load", refresh);
897    if (mq.addEventListener) mq.addEventListener("change", refresh);
898  }
899
900  function initFavoriteTextClick() {
901    var row = document.querySelector(".product-block_backend .product__info-favorite");
902    if (!row) return;
903
904    var text = row.querySelector(".product__info-favorite-text");
905    var button = row.querySelector(".js-add-favorite");
906    if (!text || !button) return;
907
908    text.addEventListener("click", function () {
909      button.click();
910    });
911  }
912
913  function initScrollToTopCompatibility() {
914    var button = document.querySelector(".js-arrow-up");
915    if (!button) return;
916
917    function getScrollTop() {
918      return Math.max(
919        window.scrollY || 0,
920        document.documentElement.scrollTop || 0,
921        document.body.scrollTop || 0
922      );
923    }
924
925    function updateVisibility() {
926      document.documentElement.classList.toggle(
927        "up-visible",
928        getScrollTop() >= 600
929      );
930    }
931
932    document.addEventListener("scroll", updateVisibility, {
933      passive: true,
934      capture: true
935    });
936
937    button.addEventListener("click", function () {
938      document.body.scrollTo({
939        top: 0,
940        left: 0,
941        behavior: "smooth"
942      });
943    });
944
945    updateVisibility();
946  }
947
948  function init() {
949    initStorytellingTimeline();
950    initOfficesToggle();
951    initFooterAccordion();
952    initTopbarHide();
953    initMobileFloatingSearch();
954    initProfileEditFormScroll();
955    initNearestPharmacy();
956    initLocationsScrollOffset();
957    initProductGalleryNav();
958    initProductTabsAccordion();
959    initProductDescriptionClamp();
960    initFavoriteTextClick();
961    initMegaMenuProducts();
962    initScrollToTopCompatibility();
963  }
964
965  if (document.readyState !== "loading") init();
966  else document.addEventListener("DOMContentLoaded", init);
967})();

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.