1/** 2 * @file 3 * In-page menu functionality. 4 * 5 * Creates a scrolling, collapsible in-page menu system. 6 * 7 * In Drupal, the attach function of Drupal.behaviors executes in two primary 8 * scenarios: 9 * 10 * 1. Initial Page Load 11 * When the page first finishes loading, Drupal calls 12 * Drupal.attachBehaviors(document, drupalSettings). 13 * This triggers the attach function for every registered behavior in the 14 * system. At this stage, the context argument passed to your function is the 15 * document object. 16 * 17 * 2. After AJAX Requests 18 * This is the most critical reason for using Behaviors over standard 19 * jQuery $(document).ready(). Whenever Drupal injects new HTML into the page 20 * via AJAX (e.g., Views pagination, infinite scroll, file uploads, or modal 21 * dialogs), Drupal.attachBehaviors is called again. 22 * In these instances: 23 * The context argument is the newly injected HTML element (the container of 24 * the 25 * new content), rather than the whole document. 26 * By using this context in your selectors 27 * (e.g., $(context).find('.my-element')), 28 * you ensure your JavaScript applies to the new content without re-running on 29 * elements that were already processed. 30 */ 31(function (Drupal, window, document, once) { 32 Drupal.behaviors.topic_nav = { 33 attach: function (context, settings) { 34 35 'use strict'; 36 37 // Scroll to clicked anchor, allowing for page furniture. 38 let remLoc = ""; 39 const navMenu = document.querySelector('#topic-nav'); 40 const siteHeader = document.querySelector('#main-menu'); 41 const siteBanner = document.querySelector('.site-banner'); 42 43 // Scroll to an element on the page. 44 const scrollToAnchor = function (t) { 45 let target = t.getAttribute('href'); 46 if (target.indexOf('#') !== -1) { 47 target = target.replace('#', ''); 48 target = document.getElementById(target); 49 } 50 else if (target.indexOf('.') !== -1) { 51 target = target.replace('.', ''); 52 target = document.getElementsByClassName(target)[0]; 53 } 54 const loggedin = document.querySelector('body.logged-in'); 55 const offset = (window.innerWidth <= 480 ? 0 : (loggedin ? (navMenu.offsetHeight - 60) : -20)); 56 target.style.scrollMarginTop = navMenu.offsetHeight + offset + 'px'; 57 target.scrollIntoView({behavior: 'smooth', block: 'start'}); 58 target.style.scrollMarginTop = ''; 59 }; 60 61 // Tests if an element is in the visible window. 62 const isElementInViewport = function (element) { 63 const rect = element.getBoundingClientRect(); 64 const viewportHeight = window.innerHeight || document.documentElement.clientHeight; 65 const navMenuHeight = navMenu.offsetHeight; 66 67 // Check if the element is within the viewport 68 if (rect.bottom > 0 && rect.top < viewportHeight) { 69 return 'in'; 70 } 71 else if (rect.bottom >= 0) { 72 return 'out-below'; 73 } 74 else if (rect.top <= viewportHeight) { 75 return 'out-above'; 76 } 77 }; 78 79 // Anchors menu to top of page, and restyles. 80 const stickyNav = function () { 81 const menu = document.getElementById("topic-nav-anchor"); 82 const loc = isElementInViewport(menu); 83 if (loc !== remLoc) { 84 const intro = document.querySelectorAll('.intro-content'); 85 if (window.innerWidth >= 980 && loc === "out-above") { 86 navMenu.classList.add('sticky'); 87 intro.forEach(function(element) { 88 element.classList.add('nav-fill-margin'); 89 }); 90 } 91 else if (loc !== "out-above") { 92 navMenu.classList.remove('sticky') 93 intro.forEach(function(element) { 94 element.classList.remove('nav-fill-margin'); 95 }); 96 } 97 } 98 remLoc = loc; 99 }; 100 101 // Indicates the main in-page navigation section visible. 102 const highlightNav = function () { 103 if (navMenu.classList.contains('sticky')) { 104 const navItems = navMenu.getElementsByTagName('li'); 105 for (const element of navItems) { 106 let target = element.getElementsByTagName("a"); 107 if (target.length) { 108 target = target[0].getAttribute('href').replace('#', ''); 109 const te = document.getElementById(target);
110 if (isElementInViewport(te) === 'in') { 111 document.querySelectorAll('li a.scroll-link-js').forEach(function (element) { 112 element.classList.remove('is-active'); 113 }) 114 element.querySelectorAll('a').forEach(function (element) { 115 element.classList.add('is-active'); 116 }) 117 return false; 118 } 119 } 120 } 121 } 122 }; 123 124 let end = 0; 125 126 const drawerAnimate = function (tray, action, speed = 250) { 127 128 tray.style.transition = 'height ' + speed + 'ms ease-in-out'; 129 130 if (action === "open") { 131 tray.classList.add('open'); 132 tray.style.height = '0'; 133 tray.style.overflow = 'hidden'; 134 tray.style.display = 'block'; 135 end = tray.scrollHeight; 136 } 137 else { 138 end = 0; 139 tray.style.transition = 'height ' + speed + 'ms ease-in-out'; 140 tray.style.height = tray.scrollHeight + 'px'; 141 tray.style.overflow = 'hidden'; 142 } 143 144 requestAnimationFrame(function () { 145 tray.style.height = end + 'px'; 146 }); 147 148 setTimeout(function () { 149 tray.style.height = ''; 150 tray.style.overflow = ''; 151 tray.style.transition = ''; 152 if (action === "open") { 153 tray.setAttribute('aria-expanded', 'true'); 154 } 155 else { 156 tray.style.display = 'none'; 157 tray.setAttribute('aria-expanded', 'false'); 158 tray.classList.remove('open'); 159 } 160 }, speed); 161 162 } 163 164 // Fires once for the page. 165 once('in-page-nav', '#page', context).forEach( 166 function (element) { 167 168 let remSize = 0; 169 170 // Creates a menu item on any element it is called on. 171 if (navMenu) { 172 173 // If the menu was faded out on small screens, ensure it fades back in 174 // when resized to larger screens. 175 window.addEventListener('resize', function () { 176 if (window.innerWidth > 980 && remSize < 980) { 177 // mobile => desktop 178 document.querySelector('.sub-nav-trigger').setAttribute('aria-hidden', true); 179 navMenu.style.display = 'block'; 180 const fadeIn = setInterval(function () { 181 let opacity = parseFloat(navMenu.style.opacity || 0); 182 if (opacity < 1) { 183 opacity += 0.1; 184 navMenu.style.opacity = opacity; 185 } 186 else { 187 clearInterval(fadeIn); 188 navMenu.classList.remove('open'); 189 remLoc = ""; 190 stickyNav(); 191 highlightNav(); 192 } 193 }, 30); 194 } 195 else if (window.innerWidth < 980 && remSize > 980) { 196 // desktop => mobile 197 document.querySelector('.sub-nav-trigger').removeAttribute('aria-hidden'); 198 let opacity = 1; 199 const fadeOut = setInterval(function () { 200 if (opacity > 0) { 201 opacity -= 0.1; 202 navMenu.style.opacity = opacity; 203 } 204 else { 205 clearInterval(fadeOut); 206 navMenu.style.display = 'none'; 207 navMenu.classList.remove('open'); 208 } 209 }, 30); 210 } 211 remSize = window.innerWidth; 212 }); 213 214 // Highlight the current section showing in the menu. 215 window.addEventListener('scroll', function () { 216 stickyNav(); 217 highlightNav(); 218 }); 219 220 // Ensure the nav is set properly on open 221 stickyNav(); 222 223 // Completely remove the in-page menu if it is empty. 224 if (!document.querySelector('#topic-nav li')) { 225 navMenu.remove(); 226 document.querySelectorAll('.section-nav-button-container, .sub-nav-trigger').forEach((el)=>{el.remove();}) 227 } 228 229 // Call the resize event to ensure page is set properly. 230 const resizeEvent = new Event('resize'); 231 window.dispatchEvent(resizeEvent); 232 } 233 } 234 ); //once 235 236 // Set the listeners on the form. 237 once('topic_nav', '#topic-nav', context).forEach( 238 function (element) { 239 // Fade in/out topic nav when .sub-nav-button link is clicked. 240 element.parentElement.querySelectorAll('.sub-nav-button, .sub-nav-trigger').forEach((drw) => { 241 drw.addEventListener('click', function (event) { 242 // Mobile only, the drawers are hidden on desktop. 243 const icon = this.querySelector('.ssm-chevron'); 244 const buttons = this.parentElement.querySelector('#topic-nav'); 245 const drawer = this; 246 if (drawer.classList.contains('open')) { 247 drawerAnimate(buttons, 'close', 400); 248 setTimeout(() => { 249 drawer.classList.remove('open', 'active'); 250 drawer.setAttribute('aria-expanded', 'false'); 251 }, 400); 252 } 253 else { 254 drawer.classList.add('open', 'active'); 255 drawer.setAttribute('aria-expanded', 'true'); 256 drawerAnimate(buttons, 'open', 400); 257 } 258 }); 259 }); 260 // When the table-nav link is clicked, scroll to table section. 261 element.parentElement.querySelectorAll('li:has(.scroll-link-js)').forEach((btn) => { 262 btn.addEventListener('click', function (e) { 263 e.preventDefault(); 264 const drw_tgt = this.getElementsByTagName('a'); 265 if (document.querySelector('.intro-content')) { 266 document.querySelector('.intro-content').classList.add('nav-fill-margin'); 267 } 268 const scrollTarget = btn.querySelector('.scroll-link-js').getAttribute('href'); 269 window.history.pushState({}, '', scrollTarget); 270 if (window.innerWidth < 980) { 271 const topicNav = document.getElementById('topic-nav'); 272 const subNavTrigger = document.querySelector('.sub-nav-trigger'); 273 274 let opacity = (topicNav.style.display === 'none' ? 0 : 1); 275 const targetOpacity = (opacity === 0 ? 1 : 0); 276 277 if (targetOpacity === 1) { 278 topicNav.style.display = 'block'; 279 } 280 281 topicNav.style.transition = 'opacity 400ms ease-in-out'; 282 topicNav.style.opacity = targetOpacity; 283 scrollToAnchor(drw_tgt[0]); 284 subNavTrigger.classList.toggle('active'); 285 286 window.setTimeout(function(){ 287 let opacity = topicNav.style.opacity; 288 if (opacity == 0) { 289 topicNav.style.display = 'none'; 290 topicNav.style.transition = ''; 291 topicNav.style.opacity = 1; 292 } 293 topicNav.classList.toggle('open'); 294 subNavTrigger.classList.toggle('open'); 295 }, (400 + 100)); 296 297 } 298 else { 299 scrollToAnchor(drw_tgt[0]); 300 } 301 }); 302 }); 303 304 } 305 ); 306 307 } // attach 308 } // behaviour 309})(Drupal, this, this.document, once);
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.