PageSourceSearch

https://pearce-services.com/wp-content/themes/pearce/assets/js/main.js?ver=1.0.0

js pearce-services.com collected 2026-10-09 21:51:34 UTC 31,861 bytes, 872 lines download raw bytes

1// (function(){
2//     const toggle = document.querySelector('.nav-toggle');
3//     const nav = document.querySelector('.primary-nav');
4//     if(toggle && nav){
5//         toggle.addEventListener('click', () => {
6//             const open = nav.classList.toggle('open');
7//             toggle.setAttribute('aria-expanded', open ? 'true' : 'false');
8//         });
9//         // close on ESC
10//         document.addEventListener('keydown', (e) => {
11//             if(e.key === 'Escape'){ nav.classList.remove('open'); toggle.setAttribute('aria-expanded','false'); }
12//         });
13//     }
14// })();
15
16
17"use strict";
18
19document.addEventListener('DOMContentLoaded', function() {
20
21    // Helper functions
22    const $ = (selector, parent = document) => parent.querySelector(selector);
23    const $$ = (selector, parent = document) => parent.querySelectorAll(selector);
24
25    const headerHeight = function() {
26        const header = $("header.wst-header.static-header");
27        return header ? header.offsetHeight : 0;
28    };
29
30    let hH;
31
32    function resizeHeader() {
33        const e = headerHeight();
34        const hero = $(".hero");
35        const childHeader = $(".child-header");
36
37        if (hero) hero.style.paddingTop = e + "px";
38        if (childHeader) childHeader.style.paddingTop = e + "px";
39    }
40
41    setTimeout(function() {
42        hH = headerHeight();
43    }, 800);
44
45    setTimeout(function() {
46        resizeHeader();
47    }, 800);
48
49    window.addEventListener('resize', resizeHeader);
50
51    // Initial scroll check
52    const stickyHeader = $("header.sticky-header");
53    const staticHeader = $("header.static-header");
54    const subMenus = $$(".sub-menu");
55
56    if (window.scrollY > hH) {
57        if (stickyHeader) stickyHeader.classList.add("active");
58        if (staticHeader) staticHeader.classList.add("hidden");
59        subMenus.forEach(menu => menu.classList.add("scrollMenu"));
60        document.body.classList.add("sticky-active");
61    }
62
63    // Mobile menu links with hash
64    $$('#mobile-menu :not(.no-link).menu-item a[href*="#"]:not([href="#"])').forEach(link => {
65        link.addEventListener('click', function() {
66            if (this.pathname === window.location.pathname) {
67                const mobileButton = $(".mobile-button");
68                if (mobileButton) mobileButton.click();
69            }
70        });
71    });
72
73    // Prevent default for hash-only links
74    $$('.wst-header .menu .menu-item a[href="#"]').forEach(link => {
75        link.addEventListener('click', function(e) {
76            e.preventDefault();
77        });
78    });
79
80    // Scroll handler for sticky header
81    function handleScroll() {
82        if (window.scrollY > hH) {
83            if (stickyHeader) stickyHeader.classList.add("active");
84            if (staticHeader) staticHeader.classList.add("hidden");
85            subMenus.forEach(menu => menu.classList.add("scrollMenu"));
86        } else {
87            if (stickyHeader) stickyHeader.classList.remove("active");
88            if (staticHeader) staticHeader.classList.remove("hidden");
89            subMenus.forEach(menu => menu.classList.remove("scrollMenu"));
90        }
91    }
92
93    window.addEventListener('scroll', handleScroll);
94    window.addEventListener('ready', handleScroll);
95
96    // Mobile button toggle
97    const mobileButtons = $$(".mobile-button");
98    mobileButtons.forEach(mobileButton => {
99        if (mobileButton) {
100            mobileButton.addEventListener('click', function () {
101                const bars = $$(".bar");
102                const mobileMenu = $(".mobile-menu");
103                const logo = $("header .logo");
104                const html = document.documentElement;
105
106                bars.forEach(bar => {
107                    bar.classList.remove("back");
108                    if (bar.classList.contains("change")) {
109                        bar.classList.add("back");
110                    }
111                    bar.classList.toggle("change");
112                    bar.classList.toggle("active");
113                });
114
115                if (mobileMenu) mobileMenu.classList.toggle("active");
116                document.body.classList.toggle("overlay-active");
117                if (logo) logo.classList.toggle("active");
118                html.classList.toggle("mobileScroll");
119
120                if (window.scrollY > hH && stickyHeader && !stickyHeader.classList.contains("active")) {
121                    stickyHeader.classList.add("active");
122                }
123                if (window.scrollY > hH && staticHeader && !staticHeader.classList.contains("hidden")) {
124                    staticHeader.classList.add("hidden");
125                }
126            });
127        }
128    });
129
130    // Mobile overlay click
131    const mobileOverlay = $(".mobile-overlay");
132    if (mobileOverlay) {
133        mobileOverlay.addEventListener('click', function() {
134            const bars = $$(".bar");
135            const mobileMenu = $(".mobile-menu");
136            const logo = $("header .logo");
137
138            document.body.classList.remove("overlay-active");
139            if (mobileMenu) mobileMenu.classList.remove("active");
140            document.documentElement.classList.remove("mobileScroll");
141            if (logo) logo.classList.remove("active");
142
143            bars.forEach(bar => {
144                if (bar.classList.contains("change")) {
145                    bar.classList.remove("change", "active");
146                    bar.classList.add("back");
147                }
148            });
149        });
150    }
151
152    // Add toggle buttons to mobile menu items with submenus
153    $$(".mobile-menu ul li").forEach(li => {
154        if (li.querySelector('ul')) {
155            const button = document.createElement('button');
156            button.className = 'wst-nav-toggle';
157            button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 7.33l2.829-2.83 9.175 9.339 9.167-9.339 2.829 2.83-11.996 12.17z" fill="#ffffff"/></svg>';
158            li.appendChild(button);
159        }
160    });
161
162    // Mobile nav toggle functionality
163    $$(".wst-nav-toggle").forEach(toggle => {
164        toggle.addEventListener('click', function(e) {
165            e.preventDefault();
166            this.parentElement.classList.toggle("toggled");
167
168            const subMenu = this.previousElementSibling;
169            if (subMenu && subMenu.classList.contains('sub-menu')) {
170                if (subMenu.style.display === 'none' || !subMenu.style.display) {
171                    subMenu.style.display = 'block';
172                    slideDown(subMenu);
173                } else {
174                    slideUp(subMenu);
175                }
176            }
177            return false;
178        });
179    });
180
181    // Hide mobile submenus initially
182    $$(".mobile-menu .sub-menu").forEach(menu => {
183        menu.style.display = 'none';
184    });
185
186    // Page banner functionality
187    $$(".page-banner").forEach(banner => {
188        const id = banner.getAttribute("id");
189        if (sessionStorage.getItem(id) === "hidden") {
190            banner.style.display = 'none';
191        }
192    });
193
194    $$(".page-banner .close-page-banner").forEach(closeBtn => {
195        closeBtn.addEventListener('click', function() {
196            const bannerId = this.getAttribute("data-banner");
197            const banner = $("#" + bannerId);
198            if (banner) {
199                slideUp(banner, () => {
200                    banner.style.display = 'none';
201                });
202            }
203            sessionStorage.setItem(bannerId, "hidden");
204        });
205    });
206
207    // Intersection Observer for lazy loading animations
208    const loaders = $$(".wsta");
209    const lazyOptions = {
210        rootMargin: "0px 0px 0px 0px"
211    };
212
213    const observer = new IntersectionObserver(function(entries, observerInstance) {
214        entries.forEach(function(entry) {
215            if (entry.isIntersecting) {
216                entry.target.classList.add("activate");
217                observerInstance.unobserve(entry.target);
218            }
219        });
220    }, lazyOptions);
221
222    loaders.forEach(function(loader) {
223        observer.observe(loader);
224    });
225
226    // Search Modal functionality
227    // Event listener for opening the search window
228// Get all matching buttons
229    const searchButtons = $$(".header__nav .header__tools-search");
230
231// Loop through each button and add the same event listener
232    searchButtons.forEach(button => {
233        button.addEventListener('click', function() {
234            console.log("search display");
235            $(".search-window").classList.add('window-active');
236            document.body.classList.add('hide-scroll', 'search-window-open');
237            $("#search").focus();
238        });
239    });
240    // Event listener for closing the search window
241
242    $(".search-window-screen").addEventListener('click', function (event) {
243        // Find the parent element and remove the class
244        event.target.parentElement.classList.remove('window-active');
245        $("body").classList.remove('hide-scroll', 'search-window-open');
246    });
247
248
249
250    // Hero video functionality
251    function heroVideo() {
252        setTimeout(function() {
253            if (window.innerWidth > 1024) {
254                const container = $("#heroVideoContainer");
255                const video = $("#heroVideo");
256
257                if (container && video && !video.getAttribute("src")) {
258                    const videoSrc = container.getAttribute("data-video");
259                    video.setAttribute("src", videoSrc);
260                }
261            }
262        }, 1000);
263    }
264
265    if ($(".hero.hero-video")) {
266        heroVideo();
267        window.addEventListener('orientationchange', function() {
268            heroVideo();
269        });
270    }
271
272    // Home page hero links
273    if (document.body.classList.contains("home")) {
274        const heroLinks = $$(".hero-content ul.content-bottom li a");
275
276        heroLinks.forEach(function(link) {
277            if (window.innerWidth >= 1025) {
278                link.addEventListener('mouseenter', function() {
279                    const span = this.querySelector("span");
280                    if (span) slideToggle(span);
281                });
282                link.addEventListener('mouseleave', function() {
283                    const span = this.querySelector("span");
284                    if (span) slideToggle(span);
285                });
286            } else {
287                const span = link.querySelector("span");
288                if (span) span.style.display = 'block';
289            }
290        });
291    }
292
293
294    // Tabs functionality
295    const initializeTabs = function(tabComponent) {
296
297        const tabButtons = tabComponent.querySelectorAll('.tab-component__nav-btn');
298        const tabPanels = tabComponent.querySelectorAll('.tab-component__panel');
299
300        tabButtons.forEach(button => {
301            button.addEventListener('click', () => {
302                const tabIndex = button.getAttribute('data-tab-index');
303
304                // Update buttons
305                tabButtons.forEach(btn => {
306                    btn.classList.remove('active');
307                    btn.setAttribute('aria-selected', 'false');
308                });
309                button.classList.add('active');
310                button.setAttribute('aria-selected', 'true');
311
312                // Update panels
313                tabPanels.forEach(panel => {
314                    if (panel.getAttribute('data-tab-index') === tabIndex) {
315                        panel.classList.add('active');
316                        panel.removeAttribute('hidden');
317                    } else {
318                        panel.classList.remove('active');
319                        panel.setAttribute('hidden', '');
320                    }
321                });
322            });
323
324            // Keyboard navigation
325            button.addEventListener('keydown', (e) => {
326                let newIndex;
327
328                if (e.key === 'ArrowRight') {
329                    e.preventDefault();
330                    newIndex = (Array.from(tabButtons).indexOf(button) + 1) % tabButtons.length;
331                    tabButtons[newIndex].click();
332                    tabButtons[newIndex].focus();
333                } else if (e.key === 'ArrowLeft') {
334                    e.preventDefault();
335                    newIndex = (Array.from(tabButtons).indexOf(button) - 1 + tabButtons.length) % tabButtons.length;
336                    tabButtons[newIndex].click();
337                    tabButtons[newIndex].focus();
338                }
339            });
340        });
341    };
342
343    $$(".tab-component__container").forEach(function(tabs) {
344        initializeTabs(tabs);
345    });
346
347    // Accordion functionality
348    const initializeAccordion = function(accordion) {
349        const accordionItems = accordion.querySelectorAll('.accordion-item');
350        const images = accordion.querySelectorAll('.accordion-image');
351        let currentImageIndex = 0;
352        accordionItems.forEach(function(item,index) {
353            // if (!item.classList.contains("active")) {
354            //     const slide = item.querySelector(".image-container");
355            //     if (slide) slide.style.display = 'none';
356            // }
357            const title = item.querySelector('.accordion-title');
358            const icon = item.querySelector('.accordion-icon');
359
360            title.addEventListener('click', function() {
361                const isActive = item.classList.contains('active');
362
363                if (isActive) {
364                    item.classList.remove('active');
365                    icon.textContent = '+';
366                } else {
367                    // Remove active class from all items
368                    accordionItems.forEach(i => {
369                        i.classList.remove('active');
370                        i.querySelector('.accordion-icon').textContent = '+';
371                    });
372
373                    // Add active class to clicked item
374                    item.classList.add('active');
375                    icon.textContent = '−';
376
377                    // Change image with slide animation
378                    if (window.innerWidth >= 768 && currentImageIndex !== index) {
379                        const currentImage = images[currentImageIndex];
380                        const newImage = images[index];
381
382                        // Exit current image (slide right)
383                        currentImage.classList.add('exit');
384                        currentImage.classList.remove('active');
385
386                        // Enter new image (slide from left)
387                        setTimeout(() => {
388                            newImage.classList.add('active');
389                            newImage.classList.remove('exit');
390                        }, 50);
391
392                        // Clean up exit class after animation
393                        setTimeout(() => {
394                            currentImage.classList.remove('exit');
395                        }, 550);
396
397                        currentImageIndex = index;
398                    }
399                }
400            });
401
402
403
404        });
405
406
407    };
408
409    $$(".accordion-container").forEach(function(acc) {
410        initializeAccordion(acc);
411    });
412
413    // Post Slider (requires Slick alternative)
414
415
416    // Testimonial Block (requires Slick alternative)
417    const testimonialBlock = function(container) {
418        console.warn('Testimonial slider requires a carousel library - Slick equivalent needed');
419    };
420
421    $$(".testimonial-con").forEach(function(testimonial) {
422        testimonialBlock(testimonial);
423    });
424
425    // Video Modal functionality
426    const initializeVideo = function(showcase) {
427        const modal = showcase.querySelector('.videoModal');
428        const videoContainer = showcase.querySelector('.video-container');
429        const closeBtn = showcase.querySelector('.closeModal');
430        const triggers = showcase.querySelectorAll('.video-trigger');
431
432        // Check if all necessary elements exist before adding listeners
433        if (!modal || !videoContainer || !closeBtn || triggers.length === 0) {
434            console.error("Missing modal, video container, close button, or triggers in showcase.", showcase);
435            return;
436        }
437
438        function openModal(e) {
439            const trigger = e.currentTarget;
440            const videoType = trigger.dataset.videoType;
441            const videoId = trigger.dataset.videoId;
442            const videoSrc = trigger.dataset.videoSrc;
443
444            let videoHTML = '';
445
446            switch(videoType) {
447                case 'youtube':
448                    videoHTML = `<iframe src="https://www.youtube.com/embed/${videoId}?autoplay=1" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>`;
449                    break;
450                case 'vimeo':
451                    videoHTML = `<iframe src="https://player.vimeo.com/video/${videoId}?autoplay=1" frameborder="0" allow="autoplay; fullscreen" allowfullscreen></iframe>`;
452                    break;
453                case 'html5':
454                    videoHTML = `<video controls autoplay><source src="${videoSrc}" type="video/mp4">Your browser does not support the video tag.</video>`;
455                    break;
456            }
457
458            videoContainer.innerHTML = videoHTML; // Corrected: Use scoped variable
459            modal.classList.add('active'); // Corrected: Use scoped variable
460            document.body.style.overflow = 'hidden';
461        }
462
463        function closeModal() {
464            modal.classList.remove('active'); // Corrected: Use scoped variable
465            // Clear video to stop playback
466            setTimeout(() => {
467                videoContainer.innerHTML = ''; // Corrected: Use scoped variable
468            }, 300);
469            document.body.style.overflow = '';
470        }
471
472        // Add click events to all triggers
473        triggers.forEach(trigger => {
474            trigger.addEventListener('click', (e) => openModal(e));
475        });
476
477        // Close button event
478        closeBtn.addEventListener('click', () => closeModal());
479
480        // Close on overlay click
481        modal.addEventListener('click', (e) => {
482            if (e.target === modal) {
483                closeModal();
484            }
485        });
486
487        // Close on Escape key
488        document.addEventListener('keydown', (e) => {
489            if (e.key === 'Escape' && modal.classList.contains('active')) {
490                closeModal();
491            }
492        });
493    };
494
495    $$(".video-wrapper").forEach(function(showcase) {
496        initializeVideo(showcase);
497    });
498
499
500    // Carrousel initialization
501
502    const initializeCarrousel =  function(carousel)  {
503        let currentIndex = 0;
504        let itemsPerView = 1;
505
506        const track = carousel.querySelector('.carousel-track');
507        const cards =carousel.querySelectorAll('.carousel-card');
508        const indicatorsContainer = carousel.querySelector('.carousel-indicators');
509        const prevBtn = carousel.querySelector('.carousel-btn.prev');
510        const nextBtn = carousel.querySelector('.carousel-btn.next');
511
512        // Configuration: Set to true for wrap-around loop, false to stop at edges
513        const enableLoop = true;
514
515        function getItemsPerView() {
516            if (window.innerWidth >= 1024) {
517                return 4;
518            } else if (window.innerWidth >= 768) {
519                return 3;
520            }
521            return 1;
522        }
523
524
525        function getMaxIndex() {
526            return cards.length  ;
527        }
528
529        function updateCarousel() {
530            if (!track) return;
531
532            const cardWidth = 100 / itemsPerView;
533            const offset = -(currentIndex * cardWidth);
534            track.style.transform = `translateX(${offset}%)`;
535            updateIndicators();
536        }
537
538        function createIndicators() {
539            if (!indicatorsContainer) return;
540
541            indicatorsContainer.innerHTML = ' ';
542            const totalIndicators = getMaxIndex() ;
543
544            for (let i = 0; i < totalIndicators; i++) {
545                const indicator = document.createElement('button');
546                indicator.className = 'indicator';
547                indicator.onclick = () => goToSlide(i);
548                indicatorsContainer.appendChild(indicator);
549            }
550            updateIndicators();
551        }
552
553        function updateIndicators() {
554            if (!indicatorsContainer) return;
555
556            const indicators = indicatorsContainer.querySelectorAll('.indicator');
557            indicators.forEach((indicator, index) => {
558                indicator.classList.toggle('active', index === currentIndex);
559            });
560        }
561
562        function moveCarousel(direction) {
563            const maxIndex = getMaxIndex();
564            currentIndex += direction;
565
566            if (currentIndex < 0) {
567                currentIndex = maxIndex-1;
568            } else if (currentIndex >= maxIndex) {
569                currentIndex = 0;
570            }
571
572            updateCarousel();
573        }
574
575        function goToSlide(index) {
576            currentIndex = index;
577            updateCarousel();
578        }
579
580        function handleResize() {
581            const newItemsPerView = getItemsPerView();
582            if (newItemsPerView !== itemsPerView) {
583                itemsPerView = newItemsPerView;
584                currentIndex = 0;
585                createIndicators();
586                updateCarousel();
587            }
588        }
589
590        // Initialize
591        itemsPerView = getItemsPerView();
592        createIndicators();
593        updateCarousel();
594
595        // Event listeners for navigation buttons
596        prevBtn.addEventListener('click', () => moveCarousel(-1));
597        nextBtn.addEventListener('click', () => moveCarousel(1));
598
599        // Handle window resize
600        window.addEventListener('resize', handleResize);
601
602        // Touch support for mobile
603        let touchStartX = 0;
604        let touchEndX = 0;
605
606        track.addEventListener('touchstart', (e) => {
607            touchStartX = e.changedTouches[0].screenX;
608        });
609
610        track.addEventListener('touchend', (e) => {
611            touchEndX = e.changedTouches[0].screenX;
612            handleSwipe();
613        });
614        function handleSwipe() {
615            if (touchStartX - touchEndX > 50) {
616                moveCarousel(1);
617            }
618            if (touchEndX - touchStartX > 50) {
619                moveCarousel(-1);
620            }
621        }
622    };
623
624    $$(".carousel-container").forEach(function(carousel) {
625        initializeCarrousel(carousel);
626    });
627
628
629    // Masonry Image Gallery - part of text and image component
630    const initializeMasonry = function (gallery) {
631        const grids = $$('[data-masonry]');
632
633        grids.forEach(grid => {
634            // Simple masonry layout using CSS Grid
635            const items = grid.querySelectorAll('.two-column-gallery__item');
636
637            items.forEach(item => {
638                const img = item.querySelector('img');
639
640                img.addEventListener('load', function() {
641                    const height = this.naturalHeight;
642                    const width = this.naturalWidth;
643                    const ratio = height / width;
644
645                    // Assign span based on aspect ratio
646                    if (ratio > 1.5) {
647                        item.classList.add('tall');
648                    } else if (ratio > 1) {
649                        item.classList.add('medium');
650                    } else {
651                        item.classList.add('standard');
652                    }
653                });
654
655                // Trigger if already loaded
656                if (img.complete) {
657                    img.dispatchEvent(new Event('load'));
658                }
659            });
660        });
661
662
663    }
664
665    $$(".two-column-gallery__gallery").forEach(function(gallery) {
666        initializeMasonry(gallery);
667    });
668
669
670
671    // Anime.js parallax gallery setup
672    if (typeof anime !== 'undefined') {
673        anime.set(".para-gallery.left .para-gall-image", {
674            translateX: "-100vh",
675            translateY: "0vh"
676        });
677
678        anime.set(".para-gallery.right .para-gall-image", {
679            translateX: "100vh",
680            translateY: "0vh"
681        });
682    }
683
684    const paraImages = $$(".para-gallery");
685    let thresh = 0.4;
686
687    if (window.innerWidth < 1024) {
688        thresh = 0.2;
689    }
690
691    const paraOptions = {
692        rootMargin: "0px 0px 0px 0px",
693        threshold: thresh
694    };
695
696    const paraImagesObserver = new IntersectionObserver(function(entries, paraImagesObserver) {
697        entries.forEach(function(entry) {
698            if (entry.isIntersecting && typeof anime !== 'undefined') {
699                const images = entry.target.querySelectorAll(".para-gall-image");
700                const heroTL = anime.timeline({
701                    easing: "easeInOutExpo"
702                });
703
704                heroTL.add({
705                    targets: images,
706                    translateX: "0",
707                    duration: 2500,
708                    delay: anime.stagger(100, {
709                        start: 0
710                    })
711                }, 0);
712
713                images.forEach(function(image) {
714                    const animetl = anime.timeline({
715                        autoplay: false
716                    });
717
718                    const paraSpeed = image.getAttribute("parallax-speed") || 1;
719                    const properties = {
720                        targets: image,
721                        easing: "linear",
722                        duration: 3000
723                    };
724
725                    heroTL.finished.then(function() {
726                        const parallaxAttr = image.getAttribute("parallax");
727                        if (parallaxAttr) {
728                            parallaxAttr.split(/[,\s]+(?={)/).forEach(function(value) {
729                                try {
730                                    Object.assign(properties, eval("(" + value + ")"));
731                                } catch(e) {
732                                    console.error("Error parsing parallax value:", e);
733                                }
734                            });
735                        }
736
737                        animetl.add(properties);
738
739                        const baselineScrollTop = window.scrollY;
740
741                        const parallaxSetup = function() {
742                            const currentScroll = window.scrollY;
743                            animetl.seek(animetl.duration * (0.5 * (currentScroll - baselineScrollTop) * (0.001 * paraSpeed)));
744                        };
745
746                        parallaxSetup();
747                        window.addEventListener('resize', parallaxSetup);
748                        window.addEventListener('scroll', parallaxSetup);
749                    });
750                });
751
752                paraImagesObserver.unobserve(entry.target);
753            }
754        });
755    }, paraOptions);
756
757    paraImages.forEach(function(image) {
758        paraImagesObserver.observe(image);
759    });
760
761    // Bottom area paragraph cleanup
762    const bottomPs = $$(".bottom_area p");
763    bottomPs.forEach(function(p) {
764        const text = p.textContent.trim();
765        if (text.length <= 0 || text === "  ") {
766            p.style.display = 'none';
767        }
768    });
769
770    // Hover box functionality
771    function enableHoverBox() {
772        const hoverBoxes = $$(".wst-hover-box");
773
774        if (window.innerWidth >= 1025) {
775            $$(".hover-content").forEach(content => {
776                slideUp(content);
777            });
778
779            hoverBoxes.forEach(function(box) {
780                // Remove existing listeners
781                const newBox = box.cloneNode(true);
782                box.parentNode.replaceChild(newBox, box);
783
784                newBox.addEventListener('mouseenter', function() {
785                    this.classList.add("hover-active");
786                    const hoverContent = this.querySelector(".hover-content");
787                    if (hoverContent) slideToggle(hoverContent);
788                });
789
790                newBox.addEventListener('mouseleave', function() {
791                    this.classList.remove("hover-active");
792                    const hoverContent = this.querySelector(".hover-content");
793                    if (hoverContent) slideToggle(hoverContent);
794                });
795            });
796        } else {
797            $$(".hover-content").forEach(content => {
798                content.style.display = 'block';
799            });
800        }
801    }
802
803    enableHoverBox();
804    window.addEventListener('resize', enableHoverBox);
805
806    // Helper functions for animations
807    function slideUp(element, callback) {
808        element.style.height = element.scrollHeight + 'px';
809        element.style.transitionProperty = 'height, margin, padding';
810        element.style.transitionDuration = '300ms';
811        element.offsetHeight; // trigger reflow
812        element.style.overflow = 'hidden';
813        element.style.height = 0;
814        element.style.paddingTop = 0;
815        element.style.paddingBottom = 0;
816        element.style.marginTop = 0;
817        element.style.marginBottom = 0;
818
819        setTimeout(() => {
820            element.style.display = 'none';
821            element.style.removeProperty('height');
822            element.style.removeProperty('padding-top');
823            element.style.removeProperty('padding-bottom');
824            element.style.removeProperty('margin-top');
825            element.style.removeProperty('margin-bottom');
826            element.style.removeProperty('overflow');
827            element.style.removeProperty('transition-duration');
828            element.style.removeProperty('transition-property');
829            if (callback) callback();
830        }, 300);
831    }
832
833    function slideDown(element, callback) {
834        element.style.removeProperty('display');
835        let display = window.getComputedStyle(element).display;
836        if (display === 'none') display = 'block';
837        element.style.display = display;
838
839        const height = element.scrollHeight;
840        element.style.overflow = 'hidden';
841        element.style.height = 0;
842        element.style.paddingTop = 0;
843        element.style.paddingBottom = 0;
844        element.style.marginTop = 0;
845        element.style.marginBottom = 0;
846        element.offsetHeight; // trigger reflow
847        element.style.transitionProperty = 'height, margin, padding';
848        element.style.transitionDuration = '300ms';
849        element.style.height = height + 'px';
850        element.style.removeProperty('padding-top');
851        element.style.removeProperty('padding-bottom');
852        element.style.removeProperty('margin-top');
853        element.style.removeProperty('margin-bottom');
854
855        setTimeout(() => {
856            element.style.removeProperty('height');
857            element.style.removeProperty('overflow');
858            element.style.removeProperty('transition-duration');
859            element.style.removeProperty('transition-property');
860            if (callback) callback();
861        }, 300);
862    }
863
864    function slideToggle(element, callback) {
865        if (window.getComputedStyle(element).display === 'none') {
866            slideDown(element, callback);
867        } else {
868            slideUp(element, callback);
869        }
870    }
871
872});

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.