11import { EventHandler } from "./bootstrap-helper" ;
2-
2+ import { throttle } from "@asu/shared" ;
3+
4+ /**
5+ * Initializes the anchor menu functionality.
6+ *
7+ * @param {string } idPrefix - The prefix for the IDs of the anchor menu elements
8+ * @returns {void }
9+ */
310function initAnchorMenu ( ) {
411 const HEADER_IDS = [ "asu-header" , "asuHeader" ] ;
12+ const SCROLL_DELAY = 100 ;
513
614 const globalHeaderId = HEADER_IDS . find ( id => document . getElementById ( id ) ) ;
7-
8- if ( globalHeaderId === undefined ) {
9- // Asu header not found in the DOM.
10- return ;
11- }
12-
1315 const globalHeader = document . getElementById ( globalHeaderId ) ;
1416 const navbar = document . getElementById ( "uds-anchor-menu" ) ;
1517 const navbarOriginalParent = navbar . parentNode ;
1618 const navbarOriginalNextSibling = navbar . nextSibling ;
1719 const anchors = navbar . getElementsByClassName ( "nav-link" ) ;
1820 const anchorTargets = new Map ( ) ;
1921 let previousScrollPosition = window . scrollY ;
20- let isNavbarAttached = false ; // Flag to track if navbar is attached to header
21- const body = document . body ;
22+ let isNavbarAttached = false ;
2223
2324 // These values are for optionally present Drupal admin toolbars. They
2425 // are not present in Storybook and not required in implementations.
25- let toolbarBar = document . getElementById ( "toolbar-bar" ) ;
26- let toolbarItemAdministrationTray = document . getElementById (
27- "toolbar-item-administration-tray"
28- ) ;
29-
30- let toolbarBarHeight = toolbarBar ? toolbarBar . offsetHeight : 0 ;
31- let toolbarItemAdministrationTrayHeight = toolbarItemAdministrationTray
32- ? toolbarItemAdministrationTray . offsetHeight
33- : 0 ;
26+ const toolbarBarHeight =
27+ document . getElementById ( "toolbar-bar" ) ?. offsetHeight || 0 ;
28+ const toolbarItemAdministrationTrayHeight =
29+ document . getElementById ( "toolbar-item-administration-tray" ) ?. offsetHeight ||
30+ 0 ;
3431
35- let combinedToolbarHeightOffset =
32+ const combinedToolbarHeightOffset =
3633 toolbarBarHeight + toolbarItemAdministrationTrayHeight ;
3734 const navbarInitialTop =
3835 navbar . getBoundingClientRect ( ) . top +
@@ -46,60 +43,113 @@ function initAnchorMenu() {
4643 anchorTargets . set ( anchor , target ) ;
4744 }
4845
49- /*
50- Bootstrap needs to be loaded as a variable in order for this to work.
51- An alternative is to remove this and add the data-bs-spy="scroll" data-bs-target="#uds-anchor-menu nav" attributes to the body tag
52- See https://getbootstrap.com/docs/5.3/components/scrollspy/ for more info
53- */
54- const scrollSpy = new bootstrap . ScrollSpy ( body , {
55- target : "#uds-anchor-menu nav" ,
56- rootMargin : "20%" ,
57- } ) ;
58-
5946 const shouldAttachNavbarOnLoad = window . scrollY > navbarInitialTop ;
6047 if ( shouldAttachNavbarOnLoad ) {
6148 globalHeader . appendChild ( navbar ) ;
6249 isNavbarAttached = true ;
6350 navbar . classList . add ( "uds-anchor-menu-attached" ) ;
6451 }
6552
66- window . addEventListener (
67- "scroll" ,
68- function ( ) {
69- const navbarY = navbar . getBoundingClientRect ( ) . top ;
70- const headerHeight = globalHeader . classList . contains ( "scrolled" )
71- ? globalHeader . offsetHeight - 32
72- : globalHeader . offsetHeight ; // 32 is the set height of the gray toolbar above the global header.
53+ /**
54+ * Calculates the percentage of an element that is visible in the viewport.
55+ *
56+ * @param {Element } el The element to calculate the visible percentage for.
57+ * @return {number } The percentage of the element that is visible in the viewport.
58+ */
59+ function calculateVisiblePercentage ( el ) {
60+ if ( el . offsetHeight === 0 || el . offsetWidth === 0 ) {
61+ return calculateVisiblePercentage ( el . parentElement ) ;
62+ }
63+ const rect = el . getBoundingClientRect ( ) ;
64+ const windowHeight =
65+ window . innerHeight || document . documentElement . clientHeight ;
66+ const windowWidth =
67+ window . innerWidth || document . documentElement . clientWidth ;
68+
69+ const elHeight = rect . bottom - rect . top ;
70+ const elWidth = rect . right - rect . left ;
71+
72+ const elArea = elHeight * elWidth ;
73+
74+ // Calculate the visible area of the element in the viewport
75+ const visibleHeight =
76+ Math . min ( windowHeight , rect . bottom ) - Math . max ( 0 , rect . top ) ;
77+ const visibleWidth =
78+ Math . min ( windowWidth , rect . right ) - Math . max ( 0 , rect . left ) ;
79+ const visibleArea = visibleHeight * visibleWidth ;
80+
81+ // Calculate the percentage of the element that is visible in the viewport
82+ const visiblePercentage = ( visibleArea / elArea ) * 100 ;
83+ return visiblePercentage ;
84+ }
7385
74- // If scrolling DOWN and navbar touches the globalHeader
75- if (
76- window . scrollY > previousScrollPosition &&
77- navbarY > 0 &&
78- navbarY < headerHeight
79- ) {
80- if ( ! isNavbarAttached ) {
81- // Attach navbar to globalHeader
82- globalHeader . appendChild ( navbar ) ;
83- isNavbarAttached = true ;
84- navbar . classList . add ( "uds-anchor-menu-attached" ) ;
85- }
86- previousScrollPosition = window . scrollY ;
86+ const scrollHandlerLogic = function ( ) {
87+ // Custom code added for Drupal - Handle active anchor highlighting
88+ let maxVisibility = 0 ;
89+ let mostVisibleElementId = null ;
90+
91+ // Find the element with highest visibility
92+ Array . from ( anchors ) . forEach ( anchor => {
93+ let elementId = anchor . getAttribute ( "href" ) . replace ( "#" , "" ) ;
94+ let el = document . getElementById ( elementId ) ;
95+ const visiblePercentage = calculateVisiblePercentage ( el ) ;
96+ if ( visiblePercentage > 0 && visiblePercentage > maxVisibility ) {
97+ maxVisibility = visiblePercentage ;
98+ mostVisibleElementId = el . id ;
8799 }
100+ } ) ;
101+
102+ // Update active class if we found a visible element
103+ if ( mostVisibleElementId ) {
104+ document
105+ . querySelector ( '[href="#' + mostVisibleElementId + '"]' )
106+ . classList . add ( "active" ) ;
107+ navbar
108+ . querySelectorAll (
109+ `nav > a.nav-link:not([href="#` + mostVisibleElementId + '"])'
110+ )
111+ . forEach ( function ( e ) {
112+ e . classList . remove ( "active" ) ;
113+ } ) ;
114+ }
115+
116+ // Handle navbar attachment/detachment
117+ const navbarY = navbar . getBoundingClientRect ( ) . top ;
118+ const headerBottom = globalHeader . getBoundingClientRect ( ) . bottom ;
119+ const isScrollingDown = window . scrollY > previousScrollPosition ;
120+
121+ // If scrolling DOWN and the bottom of globalHeader touches or overlaps the top of navbar
122+ if ( isScrollingDown && headerBottom >= navbarY ) {
123+ if ( ! isNavbarAttached ) {
124+ // Attach navbar to globalHeader
125+ globalHeader . appendChild ( navbar ) ;
126+ isNavbarAttached = true ;
127+ navbar . classList . add ( "uds-anchor-menu-attached" ) ;
128+ }
129+ }
130+
131+ // If scrolling UP and the header bottom no longer overlaps with the navbar
132+ if ( ! isScrollingDown && isNavbarAttached ) {
133+ const currentHeaderBottom = globalHeader . getBoundingClientRect ( ) . bottom ;
134+ const navbarCurrentTop = navbar . getBoundingClientRect ( ) . top ;
88135
89- // If scrolling UP and past the initial navbar position
136+ // Only detach if we're back to the initial navbar position or if header no longer overlaps navbar
90137 if (
91- window . scrollY < previousScrollPosition &&
92- window . scrollY <= navbarInitialTop &&
93- isNavbarAttached
138+ window . scrollY <= navbarInitialTop ||
139+ currentHeaderBottom < navbarCurrentTop
94140 ) {
95- // Detach navbar and return to original position
96141 navbarOriginalParent . insertBefore ( navbar , navbarOriginalNextSibling ) ;
97142 isNavbarAttached = false ;
98143 navbar . classList . remove ( "uds-anchor-menu-attached" ) ;
99144 }
145+ }
100146
101- previousScrollPosition = window . scrollY ;
102- } ,
147+ previousScrollPosition = window . scrollY ;
148+ } ;
149+
150+ window . addEventListener (
151+ "scroll" ,
152+ ( ) => throttle ( scrollHandlerLogic , SCROLL_DELAY ) ,
103153 { passive : true }
104154 ) ;
105155
@@ -108,29 +158,27 @@ function initAnchorMenu() {
108158 anchor . addEventListener ( "click" , function ( e ) {
109159 e . preventDefault ( ) ;
110160
111- // Compensate for height of navbar so content appears below it
112- let scrollBy =
113- anchorTarget . getBoundingClientRect ( ) . top - navbar . offsetHeight ;
161+ // Get current viewport height and calculate the 1/4 position so that the
162+ // top of section is visible when you click on the anchor.
163+ const viewportHeight = window . innerHeight ;
164+ const targetQuarterPosition = Math . round ( viewportHeight * 0.25 ) ;
114165
115- // If window hasn't been scrolled, compensate for header shrinking.
116- const approximateHeaderSize = 65 ;
117- if ( window . scrollY === 0 ) scrollBy += approximateHeaderSize ;
166+ const targetAbsoluteTop =
167+ anchorTarget . getBoundingClientRect ( ) . top + window . scrollY ;
118168
119- // If navbar hasn't been stickied yet, that means global header is still in view, so compensate for header height
120- if ( ! navbar . classList . contains ( "uds-anchor-menu-sticky" ) ) {
121- if ( window . scrollY > 0 ) scrollBy += 24 ;
122- scrollBy -= globalHeader . offsetHeight ;
123- }
169+ let scrollToPosition = targetAbsoluteTop - targetQuarterPosition ;
124170
125- window . scrollBy ( {
126- top : scrollBy ,
171+ window . scrollTo ( {
172+ top : scrollToPosition ,
127173 behavior : "smooth" ,
128174 } ) ;
129175
130176 // Remove active class from other anchor in navbar, and add it to the clicked anchor
131177 const active = navbar . querySelector ( ".nav-link.active" ) ;
132178
133- if ( active ) active . classList . remove ( "active" ) ;
179+ if ( active ) {
180+ active . classList . remove ( "active" ) ;
181+ }
134182
135183 e . target . classList . add ( "active" ) ;
136184 } ) ;
0 commit comments