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.