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.