1/** 2 * WikiPress Theme - Main JavaScript 3 * 4 * Lightweight vanilla JS for mobile menu, TOC, search toggle, 5 * scroll highlighting, and other interactive features. 6 * 7 * @package WikiPress 8 */ 9 10( function () { 11 'use strict'; 12 13 /** 14 * Mobile menu toggle. 15 */ 16 function initMobileMenu() { 17 var toggle = document.querySelector( '.menu-toggle' ); 18 var menu = document.getElementById( 'primary-menu' ); 19 20 if ( ! toggle || ! menu ) { 21 return; 22 } 23 24 toggle.addEventListener( 'click', function () { 25 var expanded = toggle.getAttribute( 'aria-expanded' ) === 'true'; 26 toggle.setAttribute( 'aria-expanded', expanded ? 'false' : 'true' ); 27 menu.classList.toggle( 'is-active' ); 28 } ); 29 30 // Close mobile menu when clicking outside. 31 document.addEventListener( 'click', function ( e ) { 32 if ( ! toggle.contains( e.target ) && ! menu.contains( e.target ) ) { 33 toggle.setAttribute( 'aria-expanded', 'false' ); 34 menu.classList.remove( 'is-active' ); 35 } 36 } ); 37 38 // Handle sub-menu toggles on mobile. 39 var subMenus = menu.querySelectorAll( '.menu-item-has-children > .sub-menu' ); 40 subMenus.forEach( function ( subMenu ) { 41 var parentLink = subMenu.previousElementSibling; 42 if ( parentLink && parentLink.tagName === 'A' ) { 43 parentLink.addEventListener( 'click', function ( e ) { 44 if ( window.innerWidth <= 768 ) { 45 e.preventDefault(); 46 subMenu.classList.toggle( 'is-active' ); 47 } 48 } ); 49 } 50 } ); 51 } 52 53 /** 54 * Header search toggle. 55 */ 56 function initSearchToggle() { 57 var toggle = document.querySelector( '.search-toggle' ); 58 var form = document.getElementById( 'header-search-form' ); 59 60 if ( ! toggle || ! form ) { 61 return; 62 } 63 64 toggle.addEventListener( 'click', function () { 65 var expanded = toggle.getAttribute( 'aria-expanded' ) === 'true'; 66 toggle.setAttribute( 'aria-expanded', expanded ? 'false' : 'true' ); 67 form.setAttribute( 'aria-hidden', expanded ? 'false' : 'true' ); 68 form.classList.toggle( 'is-active' ); 69 70 if ( ! expanded ) { 71 // Focus the search field when opened. 72 var searchField = form.querySelector( '.search-field' ); 73 if ( searchField ) { 74 setTimeout( function () { 75 searchField.focus(); 76 }, 100 ); 77 } 78 } 79 } ); 80 81 // Close search when clicking outside. 82 document.addEventListener( 'click', function ( e ) { 83 if ( ! toggle.contains( e.target ) && ! form.contains( e.target ) ) { 84 toggle.setAttribute( 'aria-expanded', 'false' ); 85 form.setAttribute( 'aria-hidden', 'true' ); 86 form.classList.remove( 'is-active' ); 87 } 88 } ); 89 } 90 91 /** 92 * Table of Contents toggle (mobile). 93 */ 94 function initTOCToggle() { 95 var tocToggles = document.querySelectorAll( '.toc-toggle' ); 96 97 tocToggles.forEach( function ( toggle ) { 98 toggle.addEventListener( 'click', function () { 99 var list = toggle.nextElementSibling; 100 if ( ! list ) { 101 return; 102 } 103 104 var expanded = toggle.getAttribute( 'aria-expanded' ) === 'true'; 105 toggle.setAttribute( 'aria-expanded', expanded ? 'false' : 'true' ); 106 list.classList.toggle( 'collapsed' ); 107 108 // Update toggle icon. 109 var icon = toggle.querySelector( '.toc-toggle-icon' ); 110 if ( icon ) { 111 icon.textContent = expanded ? '[+]' : '[â]'; 112 } 113 } ); 114 } ); 115 } 116 117 /** 118 * Active section highlighting in TOC during scroll. 119 * Uses IntersectionObserver for performance. 120 */ 121 function initTOCScrollHighlight() { 122 var tocLinks = document.querySelectorAll( '.toc-list a' ); 123 if ( tocLinks.length === 0 ) { 124 return; 125 } 126 127 // Get all heading elements referenced by TOC links. 128 var headingIds = []; 129 tocLinks.forEach( function ( link ) { 130 var href = link.getAttribute( 'href' ); 131 if ( href && href.startsWith( '#' ) ) { 132 headingIds.push( href.substring( 1 ) ); 133 } 134 } ); 135 136 if ( headingIds.length === 0 ) { 137 return; 138 } 139 140 var observer = new IntersectionObserver( 141 function ( entries ) { 142 entries.forEach( function ( entry ) { 143 if ( entry.isIntersecting ) { 144 // Remove active class from all TOC links. 145 tocLinks.forEach( function ( link ) { 146 link.classList.remove( 'active' ); 147 } ); 148 149 // Add active class to corresponding TOC link. 150 var activeLink = document.querySelector( 151 '.toc-list a[href="#' + CSS.escape( entry.target.id ) + '"]' 152 ); 153 if ( activeLink ) { 154 activeLink.classList.add( 'active' ); 155 } 156 } 157 } ); 158 }, 159 { 160 rootMargin: '-80px 0px -70% 0px', 161 threshold: 0, 162 } 163 ); 164 165 headingIds.forEach( function ( id ) { 166 var heading = document.getElementById( id ); 167 if ( heading ) { 168 observer.observe( heading ); 169 } 170 } ); 171 } 172 173 /** 174 * Smooth scroll for anchor links (skip link, TOC links). 175 */ 176 function initSmoothScroll() { 177 document.addEventListener( 'click', function ( e ) { 178 var link = e.target.closest( 'a[href^="#"]' ); 179 if ( ! link ) { 180 return; 181 } 182 183 var href = link.getAttribute( 'href' ); 184 if ( href === '#' ) { 185 return; 186 } 187 188 var target = document.querySelector( href ); 189 if ( ! target ) { 190 return; 191 } 192 193 // Don't intercept TOC toggle buttons or navigation links. 194 if ( link.closest( '.toc-toggle' ) || link.closest( '.menu-toggle' ) ) { 195 return; 196 } 197 198 e.preventDefault(); 199 target.scrollIntoView( { behavior: 'smooth', block: 'start' } ); 200 201 // Update URL hash without jump. 202 history.pushState( null, null, href ); 203 } ); 204 } 205 206 /** 207 * Handle keyboard navigation for collapsible elements. 208 */ 209 function initKeyboardAccessibility() { 210 // Allow Enter/Space to toggle collapsible elements. 211 document.addEventListener( 'keydown', function ( e ) { 212 if ( e.key === 'Enter' || e.key === ' ' ) { 213 var target = e.target.closest( '.toc-toggle, .menu-toggle, .search-toggle' ); 214 if ( target && target.tagName === 'BUTTON' ) { 215 // Buttons already handle Enter/Space natively. 216 return; 217 } 218 } 219 220 // Escape to close menus/search. 221 if ( e.key === 'Escape' ) { 222 var menu = document.getElementById( 'primary-menu' ); 223 var menuToggle = document.querySelector( '.menu-toggle' ); 224 var searchForm = document.getElementById( 'header-search-form' ); 225 var searchToggle = document.querySelector( '.search-toggle' ); 226 227 if ( menu && menu.classList.contains( 'is-active' ) ) { 228 menu.classList.remove( 'is-active' ); 229 if ( menuToggle ) { 230 menuToggle.setAttribute( 'aria-expanded', 'false' ); 231 menuToggle.focus(); 232 } 233 } 234 235 if ( searchForm && searchForm.classList.contains( 'is-active' ) ) { 236 searchForm.classList.remove( 'is-active' ); 237 searchForm.setAttribute( 'aria-hidden', 'true' ); 238 if ( searchToggle ) { 239 searchToggle.setAttribute( 'aria-expanded', 'false' ); 240 searchToggle.focus(); 241 } 242 } 243 } 244 } ); 245 } 246 247 /** 248 * Header scroll-shrink effect. 249 * Adds 'is-scrolled' class when the page is scrolled down. 250 */ 251 function initHeaderScroll() { 252 var header = document.querySelector( '.site-header' ); 253 if ( ! header ) { 254 return; 255 } 256 257 var scrollThreshold = 50; 258 var isScrolled = false;
259 260 function onScroll() { 261 var shouldBeScrolled = window.scrollY > scrollThreshold; 262 if ( shouldBeScrolled !== isScrolled ) { 263 isScrolled = shouldBeScrolled; 264 header.classList.toggle( 'is-scrolled', isScrolled ); 265 } 266 } 267 268 window.addEventListener( 'scroll', onScroll, { passive: true } ); 269 onScroll(); // Check initial state. 270 } 271 272 /** 273 * Initialize all features when DOM is ready. 274 */ 275 function init() { 276 initMobileMenu(); 277 initSearchToggle(); 278 initTOCToggle(); 279 initTOCScrollHighlight(); 280 initSmoothScroll(); 281 initKeyboardAccessibility(); 282 initHeaderScroll(); 283 } 284 285 // Run on DOMContentLoaded. 286 if ( document.readyState === 'loading' ) { 287 document.addEventListener( 'DOMContentLoaded', init ); 288 } else { 289 init(); 290 } 291} )();
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.