1/** 2 * Telemac Editorial - main.js 3 * Mobile menu + reading progress + TOC scrollspy 4 * Target: < 3KB minified 5 */ 6(function () { 7 'use strict'; 8 9 // ===== Mobile menu toggle ===== 10 var toggle = document.querySelector('.te-menu-toggle'); 11 var header = document.querySelector('.te-header'); 12 13 if (toggle && header) { 14 toggle.addEventListener('click', function (e) { 15 e.stopPropagation(); 16 var expanded = header.classList.toggle('te-nav-open'); 17 toggle.setAttribute('aria-expanded', expanded); 18 }); 19 20 // Close on nav link click 21 var navLinks = header.querySelectorAll('.te-nav a'); 22 for (var i = 0; i < navLinks.length; i++) { 23 navLinks[i].addEventListener('click', function () { 24 header.classList.remove('te-nav-open'); 25 toggle.setAttribute('aria-expanded', 'false'); 26 }); 27 } 28 29 // Close on outside click 30 document.addEventListener('click', function (e) { 31 if (!header.contains(e.target)) { 32 header.classList.remove('te-nav-open'); 33 toggle.setAttribute('aria-expanded', 'false'); 34 } 35 }); 36 37 // Close on ESC 38 document.addEventListener('keydown', function (e) { 39 if (e.key === 'Escape' && header.classList.contains('te-nav-open')) { 40 header.classList.remove('te-nav-open'); 41 toggle.setAttribute('aria-expanded', 'false'); 42 toggle.focus(); 43 } 44 }); 45 } 46 47 // ===== Search overlay ===== 48 var searchToggle = document.querySelector('.te-search-toggle'); 49 var searchOverlay = document.getElementById('te-search-overlay'); 50 if (searchToggle && searchOverlay && header) { 51 var searchInput = searchOverlay.querySelector('input[type="search"]'); 52 function openSearch() { 53 header.classList.add('te-search-open'); 54 searchToggle.setAttribute('aria-expanded', 'true'); 55 if (searchInput) setTimeout(function () { searchInput.focus(); }, 50); 56 } 57 function closeSearch() { 58 header.classList.remove('te-search-open'); 59 searchToggle.setAttribute('aria-expanded', 'false'); 60 } 61 searchToggle.addEventListener('click', function (e) { 62 e.stopPropagation(); 63 if (header.classList.contains('te-search-open')) closeSearch(); 64 else openSearch(); 65 }); 66 document.addEventListener('click', function (e) { 67 if (!header.classList.contains('te-search-open')) return; 68 if (!searchOverlay.contains(e.target) && !searchToggle.contains(e.target)) closeSearch(); 69 }); 70 document.addEventListener('keydown', function (e) { 71 if (e.key === 'Escape' && header.classList.contains('te-search-open')) { 72 closeSearch(); 73 searchToggle.focus(); 74 } 75 }); 76 } 77 78 // ===== Reading progress bar (single post) ===== 79 var progress = document.getElementById('te-progress'); 80 if (progress) { 81 var rafId = null; 82 function updateProgress() { 83 rafId = null; 84 var doc = document.documentElement; 85 var scrolled = window.scrollY || doc.scrollTop || 0; 86 var total = doc.scrollHeight - doc.clientHeight; 87 var pct = total > 0 ? (scrolled / total) * 100 : 0; 88 progress.style.width = Math.min(100, Math.max(0, pct)) + '%'; 89 } 90 function onScroll() { 91 if (rafId) return; 92 rafId = requestAnimationFrame(updateProgress); 93 } 94 window.addEventListener('scroll', onScroll, { passive: true }); 95 updateProgress(); 96 } 97 98 // ===== TOC scrollspy (IntersectionObserver) ===== 99 var tocLinks = document.querySelectorAll('#te-toc-list a'); 100 if (tocLinks.length > 0 && 'IntersectionObserver' in window) { 101 var tocMap = {}; 102 tocLinks.forEach(function (link) { 103 var id = link.getAttribute('href').slice(1); 104 tocMap[id] = link; 105 }); 106 107 var observed = {}; 108 var observer = new IntersectionObserver( 109 function (entries) { 110 entries.forEach(function (entry) { 111 observed[entry.target.id] = entry.isIntersecting; 112 }); 113 // Find the first visible heading 114 var active = null; 115 for (var id in observed) { 116 if (observed[id]) { active = id; break; } 117 } 118 if (!active) { 119 // Fallback: find last heading above viewport 120 var headings = document.querySelectorAll('.te-article-body h2[id]'); 121 var scrollPos = window.scrollY + 150; 122 for (var i = 0; i < headings.length; i++) { 123 if (headings[i].offsetTop <= scrollPos) active = headings[i].id; 124 } 125 } 126 tocLinks.forEach(function (link) { 127 var id = link.getAttribute('href').slice(1); 128 link.classList.toggle('active', id === active); 129 }); 130 },
131 { rootMargin: '-100px 0px -60% 0px', threshold: 0 } 132 ); 133 134 var headings = document.querySelectorAll('.te-article-body h2[id]'); 135 headings.forEach(function (h) { observer.observe(h); }); 136 } 137})();
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.