1(function () { 2 if (window.__progressBarGlobalInit) return; 3 window.__progressBarGlobalInit = true; 4 5 let cleanup = null; 6 7 function initProgressBar() { 8 // Cleanup previous instance 9 if (cleanup) { 10 cleanup(); 11 cleanup = null; 12 } 13 14 // Use requestAnimationFrame to batch DOM reads 15 requestAnimationFrame(function () { 16 // Only show progress bar on individual post pages, not homepage or category pages 17 const postContainer = document.querySelector('article.post-content, main > article:only-of-type'); 18 if (!postContainer) return; 19 20 // Additional check: don't show on homepage or pages with multiple articles 21 const allArticles = document.querySelectorAll('article'); 22 if (allArticles.length > 1) return; 23 24 // Create and insert the progress bar after the navbar 25 const navbar = document.querySelector('.navbar'); 26 let progressBar = document.getElementById('scrollProgressBar'); 27 if (!progressBar) { 28 progressBar = document.createElement('div'); 29 progressBar.id = 'scrollProgressBar'; 30 progressBar.style.cssText = 31 'position: fixed; ' + 32 'top: 0; ' + 33 'inset-inline-start: 0; ' + 34 'width: 0%; ' + 35 'z-index: var(--z-index-progress-bar);'; 36 if (navbar && navbar.nextSibling) { 37 navbar.parentNode.insertBefore(progressBar, navbar.nextSibling); 38 } else { 39 document.body.insertBefore(progressBar, document.body.firstChild); 40 } 41 } else { 42 // Reset width for new page 43 progressBar.style.width = '0%'; 44 } 45 46 let navbarHeight = 0; 47 let articleTop = 0; 48 let articleHeight = 0; 49 let windowHeight = 0; 50 let totalScrollable = 0; 51 let navbarIsFixed = false; 52 let rafId = null; 53 let scrollPending = false; 54 55 function updateLayoutMeasurements() { 56 requestAnimationFrame(function () { 57 if (navbar) { 58 navbarHeight = navbar.offsetHeight; 59 navbarIsFixed = navbar.classList.contains('fixed'); 60 if (!navbarIsFixed) { 61 const style = window.getComputedStyle(navbar); 62 navbarIsFixed = style.position === 'fixed'; 63 } 64 } 65 articleTop = postContainer.offsetTop; 66 articleHeight = postContainer.scrollHeight; 67 windowHeight = window.innerHeight;
68 const effectiveViewportHeight = navbarIsFixed ? windowHeight - navbarHeight : windowHeight; 69 totalScrollable = articleHeight - effectiveViewportHeight / 2; 70 setProgressBarPosition(); 71 }); 72 } 73 74 function setProgressBarPosition() { 75 if (!navbar || !progressBar) return; 76 const newTop = navbarIsFixed ? navbarHeight : 0; 77 if (progressBar.style.top !== newTop + 'px') { 78 progressBar.style.top = newTop + 'px'; 79 } 80 } 81 82 function updateProgressBar() { 83 if (!progressBar || scrollPending) return; 84 scrollPending = true; 85 86 rafId = requestAnimationFrame(function () { 87 const scrollTop = window.scrollY || document.documentElement.scrollTop; 88 const scrolled = scrollTop - articleTop; 89 let progress = (scrolled / totalScrollable) * 100; 90 progress = Math.max(0, Math.min(progress, 100)); 91 progressBar.style.width = progress + '%'; 92 scrollPending = false; 93 }); 94 } 95 96 let resizeTimeout; 97 function handleResize() { 98 clearTimeout(resizeTimeout); 99 resizeTimeout = setTimeout(function() { 100 updateLayoutMeasurements(); 101 updateProgressBar(); 102 }, 150); 103 } 104 105 function handleScroll() { 106 updateProgressBar(); 107 } 108 109 // Initial measurements and setup 110 updateLayoutMeasurements(); 111 updateProgressBar(); 112 113 // Add event listeners 114 window.addEventListener('resize', handleResize); 115 window.addEventListener('scroll', handleScroll, { passive: true }); 116 117 // Recalculate when article size changes 118 const resizeObserver = new ResizeObserver(function() { 119 clearTimeout(resizeTimeout); 120 resizeTimeout = setTimeout(function() { 121 updateLayoutMeasurements(); 122 updateProgressBar(); 123 }, 100); 124 }); 125 resizeObserver.observe(postContainer); 126 127 // Store cleanup function 128 cleanup = function() { 129 window.removeEventListener('resize', handleResize); 130 window.removeEventListener('scroll', handleScroll); 131 if (rafId) cancelAnimationFrame(rafId); 132 clearTimeout(resizeTimeout); 133 resizeObserver.disconnect(); 134 // Hide progress bar when leaving page 135 if (progressBar) progressBar.style.width = '0%'; 136 }; 137 }); 138 } 139 140 // Run on initial load 141 if (document.readyState === 'loading') { 142 document.addEventListener('DOMContentLoaded', initProgressBar); 143 } else { 144 initProgressBar(); 145 } 146 147 // Re-run on Turbo navigation 148 document.addEventListener('turbo:load', initProgressBar); 149})();
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.