PageSourceSearch

https://www.tyremarket.com/tyremantra/wp-content/themes/blog-over/assets/js/theme-functions.js?ver=1.2.1

js tyremarket.com collected 2026-10-03 21:32:00 UTC 37,136 bytes, 1,026 lines download raw bytes

1/**
2 * Blog Over Theme Functions
3 * 
4 * Main theme JavaScript functionality
5 *
6 * @package Blog_Over
7 */
8
9(function () {
10    'use strict';
11
12    /**
13     * Dark Mode Toggle (Top Bar Switcher)
14     * 
15     * PRIORITY LOGIC:
16     * 1. User Preference (localStorage) - highest priority
17     * 2. Theme Skin (Customizer/body class from PHP) - fallback
18     * 3. Light mode - hardcoded fallback
19     * 
20     * User preference saved to localStorage, persists across page reloads
21     * Theme Skin setting in Customizer is NEVER modified by this toggle
22     * Note: Inline script in <head> applies initial class to html element to prevent FOUC
23     */
24    function initDarkMode() {
25        var toggles = document.querySelectorAll('.blog-over-theme-toggle');
26        if (!toggles.length) return;
27
28        const STORAGE_KEY = 'blog_over_user_skin_preference';
29
30        // Read user preference from localStorage
31        const userPreference = localStorage.getItem(STORAGE_KEY);
32
33        // Get admin default from body class (set by PHP based on Customizer Theme Skin)
34        const adminDefault = document.body.classList.contains('blog-over-dark-mode') ? 'dark' : 'light';
35
36        // Determine active skin: User preference > Admin default
37        let activeSkin;
38        if (userPreference && (userPreference === 'dark' || userPreference === 'light')) {
39            activeSkin = userPreference;
40        } else {
41            activeSkin = adminDefault;
42        }
43
44        // Apply the active skin to both html and body (html is already set by inline script)
45        if (activeSkin === 'dark') {
46            document.documentElement.classList.add('blog-over-dark-mode');
47            document.body.classList.add('blog-over-dark-mode');
48        } else {
49            document.documentElement.classList.remove('blog-over-dark-mode');
50            document.body.classList.remove('blog-over-dark-mode');
51        }
52
53        // Update all toggle states
54        var isDark = activeSkin === 'dark';
55        toggles.forEach(function (toggle) {
56            toggle.setAttribute('aria-pressed', isDark.toString());
57            updateToggleIcon(toggle, isDark);
58        });
59
60        // Bind click to every toggle — syncs all instances
61        toggles.forEach(function (toggle) {
62            toggle.addEventListener('click', function () {
63                var newIsDark = !document.body.classList.contains('blog-over-dark-mode');
64
65                if (newIsDark) {
66                    document.documentElement.classList.add('blog-over-dark-mode');
67                    document.body.classList.add('blog-over-dark-mode');
68                } else {
69                    document.documentElement.classList.remove('blog-over-dark-mode');
70                    document.body.classList.remove('blog-over-dark-mode');
71                }
72
73                var newSkin = newIsDark ? 'dark' : 'light';
74                localStorage.setItem(STORAGE_KEY, newSkin);
75
76                // Update ALL toggles so desktop + mobile stay in sync
77                toggles.forEach(function (t) {
78                    t.setAttribute('aria-pressed', newIsDark.toString());
79                    updateToggleIcon(t, newIsDark);
80                });
81            });
82        });
83    }
84
85    /**
86     * Update toggle button icon/state (supports all toggle styles)
87     */
88    function updateToggleIcon(toggle, isDark) {
89        // Pill & Labeled styles: use CSS class for thumb position
90        if (toggle.classList.contains('blog-over-toggle-pill') ||
91            toggle.classList.contains('blog-over-toggle-labeled')) {
92            if (isDark) {
93                toggle.classList.add('active');
94            } else {
95                toggle.classList.remove('active');
96            }
97            return;
98        }
99
100        // Icon style: swap moon/sun icon
101        const icon = toggle.querySelector('i');
102        if (icon) {
103            if (isDark) {
104                icon.className = 'fas fa-sun'; // Show sun icon when dark (click to go light)
105            } else {
106                icon.className = 'fas fa-moon'; // Show moon icon when light (click to go dark)
107            }
108        }
109    }
110
111    /**
112     * Scroll to Top Button
113     */
114    function initScrollToTop() {
115        const btn = document.querySelector('.blog-over-scroll-top');
116        if (!btn) return;
117
118        function toggleVisibility() {
119            if (window.scrollY > 300) {
120                btn.classList.add('visible');
121            } else {
122                btn.classList.remove('visible');
123            }
124        }
125
126        window.addEventListener('scroll', toggleVisibility, { passive: true });
127
128        btn.addEventListener('click', function (e) {
129            e.preventDefault();
130            window.scrollTo({
131                top: 0,
132                behavior: 'smooth'
133            });
134        });
135    }
136
137    /**
138     * Sticky Sidebar
139     */
140    function initStickySidebar() {
141        const sidebar = document.querySelector('.blog-over-sticky-sidebar');
142        if (!sidebar) return;
143
144        const header = document.querySelector('.blog-over-site-header');
145        const headerHeight = header ? header.offsetHeight : 0;
146        const offset = headerHeight + 30;
147
148        function handleScroll() {
149            const sidebarTop = sidebar.parentElement.getBoundingClientRect().top;
150
151            if (sidebarTop < offset) {
152                sidebar.style.position = 'sticky';
153                sidebar.style.top = offset + 'px';
154            }
155        }
156
157        window.addEventListener('scroll', handleScroll, { passive: true });
158    }
159
160    /**
161     * Smooth Anchor Links
162     */
163    function initSmoothScroll() {
164        document.querySelectorAll('a[href^="#"]').forEach(function (anchor) {
165            anchor.addEventListener('click', function (e) {
166                const targetId = this.getAttribute('href');
167                if (targetId === '#') return;
168
169                const target = document.querySelector(targetId);
170                if (!target) return;
171
172                e.preventDefault();
173                target.scrollIntoView({
174                    behavior: 'smooth',
175                    block: 'start'
176                });
177            });
178        });
179    }
180
181    /**
182     * Search Functionality
183     * Handles all 3 search styles: popup, inline, slide-panel
184     */
185    function initSearch() {
186        var searchToggles = document.querySelectorAll('.blog-over-search-toggle');
187        if (!searchToggles.length) return;
188
189        const searchModal = document.querySelector('.blog-over-search-modal');
190        const searchInline = document.querySelector('.blog-over-search-inline');
191        const searchPanel = document.querySelector('.blog-over-search-panel');
192
193        function getStyleSearchElement(style) {
194            if (style === 'popup') return searchModal;
195            if (style === 'inline') return searchInline;
196            if (style === 'slide-panel' || style === 'slide-panel-rtl') return searchPanel;
197            return null;
198        }
199
200        let activeSearchElement = null;
201        let activeSearchStyle = null;
202        let activeToggle = null;
203
204        function positionInlineDropdown(toggle, element) {
205            var rect = toggle.getBoundingClientRect();
206            var dropdownWidth = 340;
207            var gap = 10;
208            var topPos = rect.bottom + gap;
209            var rightPos = Math.max(12, window.innerWidth - rect.right - 12);
210
211            // Ensure dropdown doesn't go off-screen right
212            if (rect.right - dropdownWidth + 12 < 0) {
213                rightPos = window.innerWidth - dropdownWidth - 12;
214            }
215
216            element.style.top = topPos + 'px';
217            element.style.right = rightPos + 'px';
218        }
219
220        function openSearch(toggle, style, element) {
221            activeSearchElement = element;
222            activeSearchStyle = style;
223            activeToggle = toggle;
224
225            // Position inline dropdown below the toggle button
226            if (style === 'inline') {
227                positionInlineDropdown(toggle, element);
228            }
229
230            element.setAttribute('aria-hidden', 'false');
231            toggle.setAttribute('aria-expanded', 'true');
232            toggle.classList.add('active');
233
234            if (style === 'slide-panel' || style === 'slide-panel-rtl') {
235                document.body.classList.add('blog-over-search-open');
236            }
237
238            const searchInput = element.querySelector('.blog-over-search-input');
239            setTimeout(function () {
240                if (searchInput) {
241                    searchInput.focus();
242                }
243            }, 300);
244        }
245
246        function closeSearch() {
247            if (!activeSearchElement) return;
248
249            activeSearchElement.setAttribute('aria-hidden', 'true');
250
251            searchToggles.forEach(function (t) {
252                t.setAttribute('aria-expanded', 'false');
253                t.classList.remove('active');
254            });
255
256            if (activeSearchStyle === 'slide-panel' || activeSearchStyle === 'slide-panel-rtl') {
257                document.body.classList.remove('blog-over-search-open');
258            }
259
260            if (activeToggle) {
261                activeToggle.focus();
262            }
263
264            activeSearchElement = null;
265            activeSearchStyle = null;
266            activeToggle = null;
267        }
268
269        searchToggles.forEach(function (btn) {
270            btn.addEventListener('click', function (e) {
271                e.preventDefault();
272                e.stopPropagation();
273
274                var style = btn.getAttribute('data-search-style') || 'popup';
275                var element = getStyleSearchElement(style);
276
277                if (!element) return;
278
279                var isOpen = element.getAttribute('aria-hidden') === 'false';
280
281                if (isOpen) {
282                    closeSearch();
283                } else {
284                    if (activeSearchElement) closeSearch();
285                    openSearch(btn, style, element);
286                }
287            });
288        });
289
290        document.querySelectorAll('.blog-over-search-close').forEach(function (btn) {
291            btn.addEventListener('click', function (e) {
292                e.preventDefault();
293                closeSearch();
294            });
295        });
296
297        document.querySelectorAll('.blog-over-search-modal-overlay, .blog-over-search-panel-overlay').forEach(function (overlay) {
298            overlay.addEventListener('click', function (e) {
299                e.preventDefault();
300                closeSearch();
301            });
302        });
303
304        document.addEventListener('keydown', function (e) {
305            if (e.key === 'Escape' && activeSearchElement) {
306                closeSearch();
307            }
308        });
309
310        // Reposition inline dropdown on scroll/resize
311        function handleScrollResize() {
312            if (activeSearchStyle === 'inline' && activeSearchElement && activeToggle) {
313                positionInlineDropdown(activeToggle, activeSearchElement);
314            }
315        }
316        window.addEventListener('scroll', handleScrollResize, { passive: true });
317        window.addEventListener('resize', handleScrollResize, { passive: true });
318
319        document.addEventListener('click', function (e) {
320            if (activeSearchStyle === 'inline' && activeSearchElement) {
321                var isInsideToggle = false;
322                searchToggles.forEach(function (t) {
323                    if (t.contains(e.target)) isInsideToggle = true;
324                });
325
326                if (!activeSearchElement.contains(e.target) && !isInsideToggle) {
327                    closeSearch();
328                }
329            }
330        });
331
332        function trapFocus(e, container) {
333            if (e.key !== 'Tab') return;
334
335            const focusableElements = container.querySelectorAll(
336                'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
337            );
338            if (!focusableElements.length) return;
339
340            const firstFocusable = focusableElements[0];
341            const lastFocusable = focusableElements[focusableElements.length - 1];
342
343            if (e.shiftKey) {
344                if (document.activeElement === firstFocusable) {
345                    lastFocusable.focus();
346                    e.preventDefault();
347                }
348            } else {
349                if (document.activeElement === lastFocusable) {
350                    firstFocusable.focus();
351                    e.preventDefault();
352                }
353            }
354        }
355
356        if (searchModal) {
357            searchModal.addEventListener('keydown', function (e) { trapFocus(e, searchModal); });
358        }
359        if (searchPanel) {
360            searchPanel.addEventListener('keydown', function (e) { trapFocus(e, searchPanel); });
361        }
362    }
363
364    /**
365     * Hero Slider
366     * Handles slider navigation, autoplay, dots, and effects
367     * Supports multiple sliders on the page (for Grid layout)
368     * Exposes reinit method for Customizer live preview
369     */
370
371    // Store slider instances for cleanup
372    var sliderInstances = [];
373
374    /**
375     * Destroy all slider instances (cleanup)
376     */
377    function destroyHeroSliders() {
378        sliderInstances.forEach(function (instance) {
379            if (instance.autoplayInterval) {
380                clearInterval(instance.autoplayInterval);
381            }
382            // Remove event listeners by cloning and replacing
383            if (instance.slider) {
384                instance.slider.removeAttribute('data-initialized');
385            }
386        });
387        sliderInstances = [];
388    }
389
390    /**
391     * Initialize all hero sliders
392     */
393    function initHeroSlider() {
394        const wrapper = document.querySelector('.blog-over-hero-wrapper');
395        if (!wrapper) return;
396
397        // Get all sliders on the page (main slider + slot sliders)
398        const sliders = wrapper.querySelectorAll('.blog-over-hero-slider');
399        if (sliders.length === 0) return;
400
401        // Read global/fallback settings from wrapper data attributes
402        const globalAutoplay = wrapper.dataset.autoplay === 'true';
403        const globalSpeed = parseInt(wrapper.dataset.speed, 10) || 5000;
404        const globalPauseHover = wrapper.dataset.pauseHover === 'true';
405        const globalEffect = wrapper.dataset.effect || 'slide';
406        const globalLoop = wrapper.dataset.loop !== 'false'; // default true
407
408        // Initialize each slider with its own settings (or fallback to global)
409        sliders.forEach(function (slider) {
410            // Skip if already initialized
411            if (slider.dataset.initialized === 'true') return;
412
413            // Read per-slider settings, fallback to global
414            const autoplay = slider.dataset.autoplay !== undefined ? slider.dataset.autoplay === 'true' : globalAutoplay;
415            const speed = slider.dataset.speed ? parseInt(slider.dataset.speed, 10) : globalSpeed;
416            const pauseHover = slider.dataset.pauseHover !== undefined ? slider.dataset.pauseHover === 'true' : globalPauseHover;
417            const effect = slider.dataset.effect || globalEffect;
418            const loop = slider.dataset.loop !== undefined ? slider.dataset.loop !== 'false' : globalLoop;
419
420            initSingleSlider(slider, autoplay, speed, pauseHover, effect, loop);
421            slider.dataset.initialized = 'true';
422        });
423    }
424
425    /**
426     * Reinitialize hero sliders (for Customizer live preview)
427     */
428    function reinitHeroSliders() {
429        destroyHeroSliders();
430        // Small delay to ensure DOM is updated
431        setTimeout(function () {
432            initHeroSlider();
433        }, 100);
434    }
435
436    // Expose globally for Customizer
437    window.novaHeroSlider = {
438        init: initHeroSlider,
439        destroy: destroyHeroSliders,
440        reinit: reinitHeroSliders
441    };
442
443    /**
444     * Initialize a single slider instance
445     */
446    function initSingleSlider(slider, autoplay, speed, pauseHover, effect, loop) {
447        const track = slider.querySelector('.blog-over-slider-track');
448        const slides = slider.querySelectorAll('.blog-over-hero-item');
449        const prevBtn = slider.querySelector('.blog-over-slider-prev');
450        const nextBtn = slider.querySelector('.blog-over-slider-next');
451        const dotsContainer = slider.querySelector('.blog-over-slider-dots, .blog-over-slider-thumbnails');
452        const dots = dotsContainer ? dotsContainer.querySelectorAll('.blog-over-slider-dot, .blog-over-slider-thumb') : [];
453
454        if (slides.length <= 1) return;
455
456        let currentIndex = 0;
457        let autoplayInterval = null;
458        let isPaused = false;
459
460        // Store instance for cleanup
461        var instance = {
462            slider: slider,
463            autoplayInterval: null
464        };
465        sliderInstances.push(instance);
466
467        /**
468         * Go to specific slide
469         */
470        function goToSlide(index) {
471            // Handle looping vs non-looping
472            if (loop) {
473                // Wrap around for loop mode
474                if (index < 0) index = slides.length - 1;
475                if (index >= slides.length) index = 0;
476            } else {
477                // Clamp for non-loop mode
478                if (index < 0) index = 0;
479                if (index >= slides.length) index = slides.length - 1;
480            }
481
482            // Remove active class from all slides and dots
483            slides.forEach(function (slide) {
484                slide.classList.remove('active', 'blog-over-slide-prev', 'blog-over-slide-next', 'blog-over-slide-out');
485            });
486            dots.forEach(function (dot) {
487                dot.classList.remove('active');
488            });
489
490            // Add active class to current slide and dot
491            slides[index].classList.add('active');
492            
493            // Add prev and next classes for advanced CSS effects (like Coverflow)
494            let prevIndex = index - 1;
495            if (prevIndex < 0) prevIndex = loop ? slides.length - 1 : 0;
496            if (slides[prevIndex] && prevIndex !== index) slides[prevIndex].classList.add('blog-over-slide-prev');
497
498            let nextIndex = index + 1;
499            if (nextIndex >= slides.length) nextIndex = loop ? 0 : slides.length - 1;
500            if (slides[nextIndex] && nextIndex !== index) slides[nextIndex].classList.add('blog-over-slide-next');
501
502            if (dots[index]) {
503                dots[index].classList.add('active');
504            }
505
506            // Dispatch event for Pro features (Fraction, Progressbar)
507            slider.dispatchEvent(new CustomEvent('blogOverSlideChanged', {
508                bubbles: true,
509                detail: { index: index, total: slides.length }
510            }));
511
512            // Apply effect based on setting
513            if (effect === 'slide' && track) {
514                // Slide effect: horizontal translate
515                track.style.transform = 'translateX(-' + (index * 100) + '%)';
516            } else if (effect === 'fade' || effect === 'coverflow' || effect === 'zoom') {
517                // Effects that rely purely on CSS for transition
518                // Track should not translate
519                if (track) {
520                    track.style.transform = 'translateX(0)';
521                }
522            }
523
524            currentIndex = index;
525        }
526
527        /**
528         * Next slide
529         */
530        function nextSlide() {
531            goToSlide(currentIndex + 1);
532        }
533
534        /**
535         * Previous slide
536         */
537        function prevSlide() {
538            goToSlide(currentIndex - 1);
539        }
540
541        /**
542         * Start autoplay
543         */
544        function startAutoplay() {
545            if (autoplay && !autoplayInterval) {
546                autoplayInterval = setInterval(function () {
547                    if (!isPaused) {
548                        nextSlide();
549                    }
550                }, speed);
551                // Store in instance for cleanup
552                instance.autoplayInterval = autoplayInterval;
553            }
554        }
555
556        /**
557         * Stop autoplay
558         */
559        function stopAutoplay() {
560            if (autoplayInterval) {
561                clearInterval(autoplayInterval);
562                autoplayInterval = null;
563            }
564        }
565
566        // Event listeners for arrows
567        if (prevBtn) {
568            prevBtn.addEventListener('click', function (e) {
569                e.preventDefault();
570                prevSlide();
571            });
572        }
573
574        if (nextBtn) {
575            nextBtn.addEventListener('click', function (e) {
576                e.preventDefault();
577                nextSlide();
578            });
579        }
580
581        // Event listeners for dots
582        dots.forEach(function (dot) {
583            dot.addEventListener('click', function (e) {
584                e.preventDefault();
585                var index = parseInt(dot.dataset.index, 10);
586                goToSlide(index);
587            });
588        });
589
590        // Pause on hover
591        if (pauseHover) {
592            slider.addEventListener('mouseenter', function () {
593                isPaused = true;
594            });
595
596            slider.addEventListener('mouseleave', function () {
597                isPaused = false;
598            });
599        }
600
601        // Keyboard navigation (only for focused slider)
602        slider.setAttribute('tabindex', '0');
603        slider.addEventListener('keydown', function (e) {
604            if (e.key === 'ArrowLeft') {
605                prevSlide();
606            } else if (e.key === 'ArrowRight') {
607                nextSlide();
608            }
609        });
610
611        // Touch/swipe support
612        let touchStartX = 0;
613        let touchEndX = 0;
614
615        slider.addEventListener('touchstart', function (e) {
616            touchStartX = e.changedTouches[0].screenX;
617        }, { passive: true });
618
619        slider.addEventListener('touchend', function (e) {
620            touchEndX = e.changedTouches[0].screenX;
621            handleSwipe(touchStartX, touchEndX);
622        }, { passive: true });
623
624        // Mouse drag support (desktop) - only on track, not dots/arrows
625        let mouseStartX = 0;
626        let isDragging = false;
627        let hasDragged = false; // Track if actual drag happened
628        const dragThreshold = 10; // Minimum px to consider it a drag
629
630        // Only attach drag to the track area
631        track.style.cursor = 'grab';
632
633        track.addEventListener('mousedown', function (e) {
634            // Don't drag if clicking on dots, arrows, or links
635            if (e.target.closest('.blog-over-slider-dots') ||
636                e.target.closest('.blog-over-slider-arrow') ||
637                e.target.closest('a')) {
638                return;
639            }
640
641            isDragging = true;
642            hasDragged = false;
643            mouseStartX = e.screenX;
644            track.style.cursor = 'grabbing';
645            e.preventDefault();
646        });
647
648        track.addEventListener('mousemove', function (e) {
649            if (!isDragging) return;
650
651            // Check if we've moved enough to consider it a drag
652            if (Math.abs(e.screenX - mouseStartX) > dragThreshold) {
653                hasDragged = true;
654            }
655            e.preventDefault();
656        });
657
658        track.addEventListener('mouseup', function (e) {
659            if (!isDragging) return;
660            isDragging = false;
661            track.style.cursor = 'grab';
662
663            if (hasDragged) {
664                handleSwipe(mouseStartX, e.screenX);
665            }
666        });
667
668        track.addEventListener('mouseleave', function (e) {
669            if (isDragging) {
670                isDragging = false;
671                track.style.cursor = 'grab';
672
673                if (hasDragged) {
674                    handleSwipe(mouseStartX, e.screenX);
675                }
676            }
677        });
678
679        // Prevent link clicks after dragging
680        slider.addEventListener('click', function (e) {
681            if (hasDragged && e.target.closest('a')) {
682                e.preventDefault();
683                e.stopPropagation();
684                hasDragged = false;
685            }
686        }, true);
687
688        function handleSwipe(startX, endX) {
689            var swipeThreshold = 50;
690            var diff = startX - endX;
691
692            if (Math.abs(diff) > swipeThreshold) {
693                if (diff > 0) {
694                    nextSlide(); // Swipe left = next
695                } else {
696                    prevSlide(); // Swipe right = prev
697                }
698            }
699        }
700
701        // Initialize
702        goToSlide(0);
703        startAutoplay();
704    }
705
706    /**
707     * Hide Loading Screen
708     * 
709     * Uses Customizer settings via data attributes:
710     * - data-min-duration: Minimum time (seconds) the loader stays visible
711     */
712    function hideLoadingScreen() {
713        const loadingScreen = document.querySelector('.blog-over-loading-screen');
714        if (!loadingScreen) return;
715
716        // Get minimum duration from data attribute (set by Customizer via header.php)
717        const minDuration = parseFloat(loadingScreen.getAttribute('data-min-duration')) || 0.5;
718        const minDurationMs = minDuration * 1000;
719
720        // Calculate when page started loading (approximate)
721        const pageLoadStart = performance.timing ? performance.timing.navigationStart : Date.now() - 1000;
722        const elapsed = Date.now() - pageLoadStart;
723        const remainingTime = Math.max(0, minDurationMs - elapsed);
724
725        // Wait for minimum duration, then hide
726        setTimeout(function () {
727            loadingScreen.classList.add('loaded');
728
729            // Remove from DOM after fade-out animation completes
730            setTimeout(function () {
731                if (loadingScreen.parentNode) {
732                    loadingScreen.parentNode.removeChild(loadingScreen);
733                }
734            }, 500);
735        }, remainingTime);
736    }
737
738    /**
739     * Mobile Top Bar — Bottom Popup
740     * 
741     * On mobile (≤768px), tapping the info icon opens a fixed
742     * bottom-sheet popup with social links, dark mode toggle,
743     * search, and subscribe. Includes:
744     *  - Overlay dismiss
745     *  - Close button
746     *  - ESC key close
747     *  - Focus trap (Tab / Shift+Tab cycle)
748     *  - Body scroll lock while open
749     */
750    function initTopbarToggle() {
751        var toggles = document.querySelectorAll('.blog-over-topbar-toggle');
752        var popups = document.querySelectorAll('.blog-over-topbar-popup');
753        
754        if (toggles.length === 0 || popups.length === 0) return;
755
756        toggles.forEach(function(toggle, index) {
757            // Assume 1-to-1 mapping if multiple exist, else fallback to first popup
758            var popup = popups[index] ? popups[index] : popups[0];
759            var closeBtn = popup.querySelector('.blog-over-topbar-popup-close');
760            var overlay = popup.querySelector('.blog-over-topbar-popup-overlay');
761            var panel = popup.querySelector('.blog-over-topbar-popup-panel');
762
763            function openPopup() {
764                popup.classList.add('blog-over-popup-open');
765                popup.setAttribute('aria-hidden', 'false');
766                toggle.setAttribute('aria-expanded', 'true');
767                document.body.style.overflow = 'hidden';
768
769                setTimeout(function () {
770                    if (closeBtn) closeBtn.focus();
771                }, 350);
772            }
773
774            function closePopup() {
775                popup.classList.remove('blog-over-popup-open');
776                popup.setAttribute('aria-hidden', 'true');
777                toggle.setAttribute('aria-expanded', 'false');
778                document.body.style.overflow = '';
779                toggle.focus();
780            }
781
782            function isOpen() {
783                return popup.classList.contains('blog-over-popup-open');
784            }
785
786            toggle.addEventListener('click', function (e) {
787                e.preventDefault();
788                e.stopPropagation();
789                if (isOpen()) {
790                    closePopup();
791                } else {
792                    openPopup();
793                }
794            });
795
796        // Close button click
797        if (closeBtn) {
798            closeBtn.addEventListener('click', function (e) {
799                e.preventDefault();
800                closePopup();
801            });
802        }
803
804        // Overlay click — close
805        if (overlay) {
806            overlay.addEventListener('click', function () {
807                closePopup();
808            });
809        }
810
811        // ESC key — close
812        document.addEventListener('keydown', function (e) {
813            if (e.key === 'Escape' && isOpen()) {
814                closePopup();
815            }
816        });
817
818        // Focus trap — cycle Tab / Shift+Tab within the popup panel
819        popup.addEventListener('keydown', function (e) {
820            if (e.key !== 'Tab' || !isOpen()) return;
821
822            var focusable = Array.from(panel.querySelectorAll(
823                'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
824            )).filter(function(el) {
825                return el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0;
826            });
827            if (!focusable.length) return;
828
829            var first = focusable[0];
830            var last = focusable[focusable.length - 1];
831
832            if (e.shiftKey) {
833                // Shift+Tab: wrap from first → last
834                if (document.activeElement === first) {
835                    e.preventDefault();
836                    last.focus();
837                }
838            } else {
839                // Tab: wrap from last → first
840                if (document.activeElement === last) {
841                    e.preventDefault();
842                    first.focus();
843                }
844            }
845        });
846        });
847    }
848
849    /**
850     * Skip Link Handler (WordPress Accessibility Guidelines)
851     * 
852     * After the skip link is activated:
853     * 1. Move focus to the target element
854     * 2. Blur the skip link so it hides properly
855     */
856    function initSkipLink() {
857        const skipLink = document.querySelector('.skip-link');
858        if (!skipLink) return;
859
860        skipLink.addEventListener('click', function (e) {
861            const targetId = this.getAttribute('href');
862            const target = document.querySelector(targetId);
863
864            if (target) {
865                e.preventDefault();
866
867                // Make target focusable if it isn't already
868                if (!target.hasAttribute('tabindex')) {
869                    target.setAttribute('tabindex', '-1');
870                }
871
872                // Focus the target element
873                target.focus({ preventScroll: false });
874
875                // Scroll to target
876                target.scrollIntoView({ behavior: 'smooth', block: 'start' });
877
878                // Blur the skip link to hide it
879                this.blur();
880            }
881        });
882    }
883
884    /**
885     * Ad Banner Close / Dismiss Button
886     *
887     * Handles the close button on dismissable ad banners.
888     * Uses event delegation so it works after Customizer partial refreshes.
889     */
890    function initAdBannerClose() {
891        document.addEventListener('click', function (e) {
892            var closeBtn = e.target.closest('.blog-over-ad-close');
893            if (!closeBtn) return;
894
895            e.preventDefault();
896            var banner = closeBtn.closest('.blog-over-ad-banner');
897            if (!banner) return;
898
899            // Smooth fade-out
900            banner.style.transition = 'opacity 0.4s ease, transform 0.4s ease, max-height 0.5s ease 0.2s';
901            banner.style.opacity = '0';
902            banner.style.transform = 'translateY(-10px)';
903            banner.style.maxHeight = banner.offsetHeight + 'px';
904            banner.style.overflow = 'hidden';
905
906            // Collapse height after fade
907            setTimeout(function () {
908                banner.style.maxHeight = '0';
909                banner.style.padding = '0';
910                banner.style.margin = '0';
911                banner.style.border = 'none';
912            }, 50);
913
914            // Remove from DOM
915            setTimeout(function () {
916                if (banner.parentNode) {
917                    banner.parentNode.removeChild(banner);
918                }
919
920                // If container is now empty, hide the wrapper
921                var wrappers = document.querySelectorAll('.blog-over-ad-banner-wrapper');
922                wrappers.forEach(function (wrapper) {
923                    if (!wrapper.querySelector('.blog-over-ad-banner')) {
924                        wrapper.style.display = 'none';
925                    }
926                });
927            }, 700);
928        });
929    }
930
931    /**
932     * Reading Progress Bar
933     * Highlights the reading progress of a single post based on scroll depth.
934     */
935    function initReadingProgressBar() {
936        var progressBar = document.getElementById('blog-over-reading-progress-bar');
937        if (!progressBar) return;
938
939        function updateProgress() {
940            var scrollTop = window.scrollY || document.documentElement.scrollTop;
941            var docHeight = Math.max(
942                document.body.scrollHeight, document.documentElement.scrollHeight,
943                document.body.offsetHeight, document.documentElement.offsetHeight,
944                document.body.clientHeight, document.documentElement.clientHeight
945            );
946            var winHeight = window.innerHeight;
947
948            var scrollable = docHeight - winHeight;
949            var scrollPercent = 0;
950
951            if (scrollable > 0) {
952                scrollPercent = (scrollTop / scrollable) * 100;
953            }
954
955            if (scrollPercent > 100) scrollPercent = 100;
956            if (scrollPercent < 0 || isNaN(scrollPercent)) scrollPercent = 0;
957
958            progressBar.style.width = scrollPercent + '%';
959        }
960
961        window.addEventListener('scroll', updateProgress, { passive: true });
962        // Initial call
963        updateProgress();
964    }
965
966    /**
967     * Initialize all functions on DOM ready
968     */
969    function init() {
970        initDarkMode();
971        initScrollToTop();
972        initStickySidebar();
973        initSmoothScroll();
974        initSearch();
975        initHeroSlider();
976        initTopbarToggle();
977        initSkipLink();
978        initAdBannerClose();
979        initReadingProgressBar();
980
981        // MutationObserver for Customizer partial refresh
982        // When Customizer updates the hero section via selective refresh,
983        // we need to reinitialize the sliders
984        if (typeof wp !== 'undefined' && wp.customize) {
985            // In Customizer preview, watch for DOM changes to hero wrapper
986            var heroObserver = new MutationObserver(function (mutations) {
987                mutations.forEach(function (mutation) {
988                    // Check if hero wrapper was added or modified
989                    mutation.addedNodes.forEach(function (node) {
990                        if (node.nodeType === 1) {
991                            if (node.classList && node.classList.contains('blog-over-hero-wrapper')) {
992                                // Hero section was replaced, reinit
993                                setTimeout(function () {
994                                    window.novaHeroSlider.reinit();
995                                }, 50);
996                            } else if (node.querySelector && node.querySelector('.blog-over-hero-wrapper')) {
997                                // Container with hero was added
998                                setTimeout(function () {
999                                    window.novaHeroSlider.reinit();
1000                                }, 50);
1001                            }
1002                        }
1003                    });
1004                });
1005            });
1006
1007            // Observe the body for changes
1008            heroObserver.observe(document.body, {
1009                childList: true,
1010                subtree: true
1011            });
1012        }
1013    }
1014
1015    if (document.readyState === 'loading') {
1016        document.addEventListener('DOMContentLoaded', init);
1017    } else {
1018        init();
1019    }
1020
1021    // Hide loading screen after window fully loads (including images)
1022    window.addEventListener('load', function () {
1023        hideLoadingScreen();
1024    });
1025
1026})();

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.