PageSourceSearch

https://www.boston.gov/themes/custom/bos_theme/js/inpage_menu.boston.js?tm6yxc

js boston.gov collected 2026-10-01 07:34:02 UTC 11,858 bytes, 309 lines download raw bytes

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.