1/** 2 * Main JS for the Memberlite Theme 3 */ 4document.addEventListener('DOMContentLoaded', function () { 5 initTabs(); 6 initDesktopNav(); 7 initMobileNav(); 8 initStickyNav(); 9 initStickyVariation(); 10 initBackToTop(); 11}); 12 13// âââ Tabs ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 14 15function initTabs() { 16 document.querySelectorAll('.memberlite_tabbable .memberlite_tabs li a').forEach(function (tabLink) { 17 tabLink.addEventListener('click', onTabClick); 18 }); 19 20 // Check if we should switch tab content on page load 21 const hashLink = document.querySelector('a[data-toggle="tab"][href="' + window.location.hash + '"]'); 22 if (hashLink) { 23 hashLink.click(); 24 } 25} 26 27function onTabClick(e) { 28 e.preventDefault(); 29 30 const tab = this.getAttribute('href').replace(/#/, ''); 31 const tabarea = this.closest('.memberlite_tabbable'); 32 33 setActiveTab(tabarea, tab); 34 setActiveTabLink(tabarea, this); 35 updateUrlHash(tab); 36} 37 38function setActiveTab(tabarea, tab) { 39 tabarea.querySelectorAll('.memberlite_tab_pane').forEach(function (pane) { 40 pane.style.display = 'none'; 41 pane.classList.remove('memberlite_active'); 42 }); 43 44 const activePane = document.getElementById(tab); 45 if (activePane) { 46 activePane.style.display = ''; 47 activePane.classList.add('memberlite_active'); 48 } 49} 50 51function setActiveTabLink(tabarea, activeLink) { 52 tabarea.querySelectorAll('.memberlite_tabs li').forEach(function (li) { 53 li.classList.remove('memberlite_active'); 54 }); 55 activeLink.closest('li').classList.add('memberlite_active'); 56} 57 58function updateUrlHash(hash) { 59 if (history.pushState) { 60 history.pushState(null, null, '#' + hash); 61 } else { 62 location.hash = '#' + hash; 63 } 64} 65 66// âââ Desktop Navigation ââââââââââââââââââââââââââââââââââââââââââââââââââââââ 67 68function initDesktopNav() { 69 // Both of these navigations are in the header and can have submenus. 70 const widgetSiteNav = document.querySelector('.header-widget-area'); 71 const memberNav = document.getElementById('member-navigation'); 72 const mainSiteNav = document.getElementById('site-navigation'); 73 // Memberlite Nav Menu Block 74 const blockSiteNav = document.querySelectorAll('.memberlite-nav-menu'); 75 76 if (mainSiteNav) { 77 mainSiteNav.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem); 78 initDesktopNavClickOutside(mainSiteNav); 79 } 80 81 if (blockSiteNav) { 82 blockSiteNav.forEach((blockMenu) => { 83 blockMenu.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem); 84 initDesktopNavClickOutside(blockMenu); 85 }); 86 } 87 88 if (memberNav) { 89 memberNav.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem); 90 initDesktopNavClickOutside(memberNav); 91 } 92 93 if (widgetSiteNav) { 94 widgetSiteNav.querySelectorAll('.menu-item-has-children').forEach(initDesktopNavItem); 95 initDesktopNavClickOutside(widgetSiteNav); 96 } 97} 98 99/** 100 * Collapse a single menu item's submenu and reset its toggle button. 101 * Also collapses any open nested submenus inside it so they don't appear 102 * pre-expanded if this item is reopened later. 103 * 104 * @param {HTMLElement} li - A .menu-item-has-children element. 105 */ 106function collapseDesktopNavItem(li) { 107 const btn = li.querySelector(':scope > button[aria-controls]'); 108 if (btn) { 109 btn.setAttribute('aria-expanded', 'false'); 110 } 111 112 li.querySelectorAll('.menu-item-has-children > button[aria-controls]').forEach(function (nestedBtn) { 113 nestedBtn.setAttribute('aria-expanded', 'false'); 114 }); 115} 116 117/** 118 * Collapse all open items within a container, with an optional item to skip 119 * (used to close siblings when opening a new item). 120 * 121 * @param {HTMLElement} container - The element to search within. 122 * @param {HTMLElement} [skip] - A .menu-item-has-children li to leave open. 123 */ 124function collapseAllDesktopNavItems(container, skip) { 125 container.querySelectorAll('.menu-item-has-children').forEach(function (li) { 126 if (li !== skip) { 127 collapseDesktopNavItem(li); 128 } 129 }); 130} 131 132/**
133 * Attach all event listeners to a single .menu-item-has-children element. 134 * 135 * @param {HTMLElement} li - A .menu-item-has-children element. 136 */ 137function initDesktopNavItem(li) { 138 const btn = li.querySelector(':scope > button[aria-controls]'); 139 if (!btn) { 140 return; 141 } 142 143 // Hover: mouseenter/mouseleave keep aria-expanded in sync with the visual 144 // hover state so the button is never out of step with what the user sees. 145 // This lets aria-expanded serve as the single CSS hook for showing submenus. 146 li.addEventListener('mouseenter', function () { 147 if (li.parentElement) { 148 collapseAllDesktopNavItems(li.parentElement, li); 149 } 150 btn.setAttribute('aria-expanded', 'true'); 151 }); 152 153 li.addEventListener('mouseleave', function () { 154 btn.setAttribute('aria-expanded', 'false'); 155 }); 156 157 // Button click: toggles the submenu for keyboard and pointer users. 158 // Because mouseenter already sets aria-expanded="true" before a mouse user 159 // could click, clicking while hovered correctly closes rather than 160 // "reopening" an already-visible submenu. 161 btn.addEventListener('click', function () { 162 const isExpanded = btn.getAttribute('aria-expanded') === 'true'; 163 164 if (!isExpanded) { 165 if (li.parentElement) { 166 collapseAllDesktopNavItems(li.parentElement, li); 167 } 168 btn.setAttribute('aria-expanded', 'true'); 169 } else { 170 collapseDesktopNavItem(li); 171 } 172 }); 173 174 // Escape key: close the innermost open submenu and return focus to its 175 // toggle button. stopPropagation gives one-level-at-a-time behaviour for 176 // three-level menus â a second Escape closes the next level up. 177 li.addEventListener('keydown', function (e) { 178 if (e.key !== 'Escape') { 179 return; 180 } 181 182 if (btn.getAttribute('aria-expanded') === 'true') { 183 e.stopPropagation(); 184 collapseDesktopNavItem(li); 185 btn.focus(); 186 } 187 }); 188 189 // focusout: collapse when focus leaves the <li> entirely. focusout bubbles, 190 // so one listener covers all descendants. relatedTarget is where focus is 191 // moving to â if it's still inside this <li>, we do nothing. 192 li.addEventListener('focusout', function (e) { 193 if (!li.contains(e.relatedTarget)) { 194 collapseDesktopNavItem(li); 195 } 196 }); 197} 198 199/** 200 * Collapse everything if the user clicks outside the nav entirely. 201 * 202 * @param {HTMLElement} nav 203 */ 204function initDesktopNavClickOutside(nav) { 205 document.addEventListener('click', function (e) { 206 if (!nav.contains(e.target)) { 207 collapseAllDesktopNavItems(nav); 208 } 209 }); 210} 211 212// âââ Mobile Navigation âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 213 214function initMobileNav() { 215 const mobileNav = document.getElementById('mobile-navigation'); 216 const mobilenavTrigger = document.getElementById('expand-mobile-nav'); 217 const mobileNavClose = document.getElementById('close-mobile-nav'); 218 219 if (!mobileNav || !mobilenavTrigger || !mobileNavClose) return; 220 221 mobilenavTrigger.addEventListener('click', function () { 222 openMobileNav(mobileNav, mobilenavTrigger, mobileNavClose); 223 }); 224 225 mobileNavClose.addEventListener('click', function () { 226 closeMobileNav(mobileNav, mobilenavTrigger); 227 }); 228 229 // Close the mobile menu when the Escape key is pressed 230 document.addEventListener('keydown', function (e) { 231 if (e.key === 'Escape' && mobileNav.classList.contains('open')) { 232 closeMobileNav(mobileNav, mobilenavTrigger); 233 } 234 }); 235 236 // Close the mobile menu when an anchor link is clicked 237 mobileNav.addEventListener('click', function (e) { 238 const link = e.target.closest('a'); 239 if (link && isAnchorLinkForCurrentPage(link)) { 240 closeMobileNav(mobileNav, mobilenavTrigger); 241 } 242 }); 243} 244 245function openMobileNav(mobileNav, trigger, closeBtn) { 246 const content = document.getElementById('content'); 247 const colophon = document.getElementById('colophon'); 248 249 document.body.classList.add('mobile-nav-open'); 250 if (content) content.setAttribute('inert', true); 251 if (colophon) colophon.setAttribute('inert', true); 252 mobileNav.removeAttribute('inert'); 253 mobileNav.classList.add('open'); 254 trigger.setAttribute('aria-expanded', 'true'); 255 closeBtn.focus(); 256} 257 258function closeMobileNav(mobileNav, trigger) { 259 const content = document.getElementById('content'); 260 const colophon = document.getElementById('colophon'); 261 262 document.body.classList.remove('mobile-nav-open'); 263 if (content) content.removeAttribute('inert'); 264 if (colophon) colophon.removeAttribute('inert'); 265 mobileNav.classList.remove('open'); 266 mobileNav.setAttribute('inert', true); 267 trigger.setAttribute('aria-expanded', 'false'); 268 trigger.focus(); 269} 270 271function isAnchorLinkForCurrentPage(link) { 272 return (
273 // Filter out links with no # at all, and bare # links (which have a hash of '') 274 link.hash !== '' && 275 // Rule out links to other domains that happen to have a hash, e.g. https://example.com/page#section 276 (link.origin === window.location.origin) && 277 // Rule out links to other pages on the same site, e.g. /another-page#section, 278 // which would cause a full navigation anyway and naturally close the menu 279 (link.pathname === window.location.pathname) 280 ); 281} 282 283// âââ Sticky Navigation âââââââââââââââââââââââââââââââââââââââââââââââââââââââ 284 285function initStickyNav() { 286 const stickyWrapper = document.querySelector( '.site-navigation-sticky-wrapper' ); 287 const stickyNav = document.getElementById( 'site-navigation' ); 288 289 if ( ! stickyWrapper || ! stickyNav ) return; 290 291 const navHeight = stickyNav.offsetHeight; 292 const adminBar = document.getElementById( 'wpadminbar' ); 293 const adminBarHeight = adminBar ? adminBar.offsetHeight : 0; 294 295 stickyWrapper.style.height = navHeight + 'px'; 296 stickyNav.style.height = navHeight + 'px'; 297 298 const stickyTop = stickyWrapper.getBoundingClientRect().top + window.scrollY - adminBarHeight; 299 300 window.addEventListener( 'scroll', function () { 301 updateStickyState( stickyNav, stickyTop, adminBarHeight ); 302 }, { passive: true } ); 303} 304 305function updateStickyState( stickyNav, stickyTop, adminBarHeight ) { 306 if ( window.scrollY > stickyTop ) { 307 stickyNav.classList.add( 'site-navigation-sticky' ); 308 stickyNav.style.top = adminBarHeight + 'px'; 309 } else { 310 stickyNav.classList.remove( 'site-navigation-sticky' ); 311 stickyNav.style.top = ''; 312 } 313} 314 315function initStickyVariation() { 316 if ( window.innerWidth < 768 ) return; 317 318 const stickyWrapper = document.querySelector( '.site-header-variation-sticky-wrapper' ); 319 const stickyNav = document.querySelector( '.site-header-variation' ); 320 321 if ( ! stickyWrapper || ! stickyNav ) return; 322 323 const navHeight = stickyNav.offsetHeight; 324 const adminBar = document.getElementById( 'wpadminbar' ); 325 const adminBarHeight = adminBar ? adminBar.offsetHeight : 0; 326 327 stickyWrapper.style.height = navHeight + 'px'; 328 stickyNav.style.height = navHeight + 'px'; 329 330 const stickyTop = stickyWrapper.getBoundingClientRect().top + window.scrollY - adminBarHeight; 331 332 updateStickyState( stickyNav, stickyTop, adminBarHeight ); 333 334 window.addEventListener( 'scroll', function () { 335 updateStickyState( stickyNav, stickyTop, adminBarHeight ); 336 }, { passive: true } ); 337} 338 339// âââ Back to Top âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 340 341function initBackToTop() { 342 const btn = document.querySelector('.memberlite-back-to-top.floating'); 343 if (!btn) return; 344 345 const SCROLL_THRESHOLD = 300; 346 347 function updateScrollVisibility() { 348 btn.classList.toggle('is-visible', window.scrollY > SCROLL_THRESHOLD); 349 } 350 351 // Hide the button when the site footer scrolls into view. 352 const footer = document.getElementById('colophon'); 353 if (footer) { 354 new IntersectionObserver(function (entries) { 355 if (!entries.length) return; 356 btn.classList.toggle('is-footer-visible', entries[0].isIntersecting); 357 }).observe(footer); 358 } 359 360 window.addEventListener('scroll', updateScrollVisibility, { passive: true }); 361 updateScrollVisibility(); 362 363 btn.addEventListener('click', function (e) { 364 e.preventDefault(); 365 window.scrollTo({ top: 0, behavior: 'smooth' }); 366 }); 367}
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.