1313
1414import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment' ;
1515
16- // ============================================================================
17- // State Management
18- // ============================================================================
19-
2016let isInitialized = false ;
2117let previousHash = null ;
2218
23- // ============================================================================
24- // Helper Functions
25- // ============================================================================
26-
27- /**
28- * Get all parent details elements for a given element
29- */
3019const getParentDetailsElements = function ( element ) {
3120 const parents = [ ] ;
3221 let current = element ;
@@ -39,14 +28,11 @@ const getParentDetailsElements = function(element) {
3928 return parents ;
4029} ;
4130
42- /**
43- * Open a details element and its collapsible content
44- */
4531const openDetailsElement = function ( detailsEl ) {
4632 if ( detailsEl && detailsEl . tagName === 'DETAILS' && ! detailsEl . open ) {
4733 detailsEl . open = true ;
4834 detailsEl . setAttribute ( 'data-collapsed' , 'false' ) ;
49- // Expand the collapsible content by removing inline styles
35+ // Expand collapsible content by removing inline styles
5036 const collapsibleContent = detailsEl . querySelector ( ':scope > div[style]' ) ;
5137 if ( collapsibleContent ) {
5238 collapsibleContent . style . display = 'block' ;
@@ -56,19 +42,13 @@ const openDetailsElement = function(detailsEl) {
5642 }
5743} ;
5844
59- /**
60- * Close a details element
61- */
6245const closeDetailsElement = function ( detailsEl ) {
6346 if ( detailsEl && detailsEl . tagName === 'DETAILS' && detailsEl . open ) {
6447 detailsEl . open = false ;
6548 detailsEl . setAttribute ( 'data-collapsed' , 'true' ) ;
6649 }
6750} ;
6851
69- /**
70- * Close all details except those in the keep-open set
71- */
7252const closeOtherDetails = function ( keepOpenSet ) {
7353 document . querySelectorAll ( 'details.config-field[open]' ) . forEach ( function ( el ) {
7454 if ( ! keepOpenSet . has ( el ) ) {
@@ -77,22 +57,15 @@ const closeOtherDetails = function(keepOpenSet) {
7757 } ) ;
7858} ;
7959
80- // ============================================================================
81- // Core Navigation Functions
82- // ============================================================================
83-
84- /**
85- * Highlight sidebar links based on current hash
86- */
87- const highlightActiveOnPageLink = function ( ) {
88- if ( ! location . hash ) {
60+ const highlightActiveOnPageLink = function ( hash ) {
61+ const hashToUse = hash || location . hash ;
62+ if ( ! hashToUse ) {
8963 return ;
9064 }
9165
92- const activeHash = location . hash . substring ( 1 ) ;
66+ const activeHash = hashToUse . substring ( 1 ) ;
9367 const allLinks = document . querySelectorAll ( 'a' ) ;
9468
95- // Remove active class from all links
9669 for ( let i = 0 ; i < allLinks . length ; i ++ ) {
9770 const link = allLinks [ i ] ;
9871 link . classList . remove ( 'active' ) ;
@@ -102,27 +75,21 @@ const highlightActiveOnPageLink = function() {
10275 }
10376 }
10477
105- // Add active class to links matching current hash
10678 const activeLinks = document . querySelectorAll ( "a[href='#" + activeHash + "']" ) ;
10779 for ( let i = 0 ; i < activeLinks . length ; i ++ ) {
10880 activeLinks [ i ] . classList . add ( 'active' ) ;
10981 }
11082} ;
11183
112- /**
113- * Highlight and expand details elements containing the active hash
114- */
11584const highlightDetailsOnActiveHash = function ( activeHash , doNotOpen ) {
11685 const activeAnchors = document . querySelectorAll ( ".anchor[id='" + activeHash + "']" ) ;
11786 const detailsElements = document . querySelectorAll ( 'details' ) ;
11887 const activeSectionElements = document . querySelectorAll ( '.active-section' ) ;
11988
120- // Remove active-section class from all elements
12189 for ( let i = 0 ; i < activeSectionElements . length ; i ++ ) {
12290 activeSectionElements [ i ] . classList . remove ( 'active-section' ) ;
12391 }
12492
125- // Add active-section class to elements following the active anchor
12693 for ( let i = 0 ; i < activeAnchors . length ; i ++ ) {
12794 const headline = activeAnchors [ i ] . parentElement ;
12895 const headlineRank = activeAnchors [ i ] . parentElement . nodeName . substr ( 1 ) ;
@@ -143,12 +110,10 @@ const highlightDetailsOnActiveHash = function(activeHash, doNotOpen) {
143110 }
144111 }
145112
146- // Remove active class from all details
147113 for ( let i = 0 ; i < detailsElements . length ; i ++ ) {
148114 detailsElements [ i ] . classList . remove ( 'active' ) ;
149115 }
150116
151- // Add active class and open parent details
152117 if ( activeAnchors . length > 0 ) {
153118 for ( let i = 0 ; i < activeAnchors . length ; i ++ ) {
154119 let element = activeAnchors [ i ] ;
@@ -172,18 +137,15 @@ const highlightDetailsOnActiveHash = function(activeHash, doNotOpen) {
172137 }
173138 }
174139
175- // Handle elements with matching IDs (for nested config fields)
176140 const targetElement = activeHash ? document . getElementById ( activeHash ) : null ;
177141 if ( targetElement ) {
178142 const parentDetails = getParentDetailsElements ( targetElement ) ;
179143 const keepOpenSet = new Set ( parentDetails ) ;
180144
181- // Close all other details if not in doNotOpen mode
182145 if ( ! doNotOpen ) {
183146 closeOtherDetails ( keepOpenSet ) ;
184147 }
185148
186- // Process parent details
187149 for ( let i = 0 ; i < parentDetails . length ; i ++ ) {
188150 const element = parentDetails [ i ] ;
189151 element . classList . add ( 'active' ) ;
@@ -201,99 +163,100 @@ const highlightDetailsOnActiveHash = function(activeHash, doNotOpen) {
201163 }
202164} ;
203165
204- /**
205- * Handle hash navigation - expand details and highlight links
206- */
166+ // NOTE: This does NOT scroll - scrolling is handled by DetailsClicksClient
207167const handleHashNavigation = function ( hash ) {
208168 if ( ! hash ) return ;
209169
210170 const targetId = hash . substring ( 1 ) ;
211-
212- // Expand parent details elements
213171 highlightDetailsOnActiveHash ( targetId ) ;
214-
215- // Highlight active sidebar link
216172 highlightActiveOnPageLink ( ) ;
217-
218- // Scroll to target (with delay for animation)
219- setTimeout ( ( ) => {
220- const element = document . getElementById ( targetId ) ;
221- if ( element ) {
222- const yOffset = - 280 ;
223- const y = element . getBoundingClientRect ( ) . top + window . scrollY + yOffset ;
224- window . scrollTo ( { top : y , behavior : 'smooth' } ) ;
225- }
226- } , 100 ) ;
227173} ;
228174
229- /**
230- * Initialize event handlers for hash links and history navigation
231- * Hash link clicks are handled by DetailsClicksClient to avoid race conditions
232- */
175+ // Hash link clicks are handled by DetailsClicksClient to avoid race conditions
233176const initializeEventHandlers = function ( ) {
234177 // Empty - event handlers managed by DetailsClicksClient
235178} ;
236179
237- // ============================================================================
238- // Docusaurus Lifecycle Hooks
239- // ============================================================================
240-
241180/**
242- * Called on client-side navigation ( before DOM update)
181+ * Docusaurus lifecycle hook - called before DOM update
243182 */
244183export function onRouteUpdate ( { location, previousLocation } ) {
245- // Track previous hash for comparison
246184 if ( previousLocation ) {
247185 previousHash = previousLocation . hash ;
248186 }
249187}
250188
251189/**
252- * Called after route has updated and DOM is ready
253- * This is where we safely manipulate DOM after React hydration
190+ * Docusaurus lifecycle hook - called after route updates and DOM is ready
191+ * Safely manipulates DOM after React hydration
254192 */
255193export function onRouteDidUpdate ( { location, previousLocation } ) {
256194 if ( ! ExecutionEnvironment . canUseDOM ) {
257195 return ;
258196 }
259197
260- // Initialize on first load
261198 if ( ! isInitialized ) {
262199 isInitialized = true ;
263200
264- // Wait for React hydration to complete
265201 requestAnimationFrame ( ( ) => {
266202 requestAnimationFrame ( ( ) => {
267- // Initialize event handlers
268203 initializeEventHandlers ( ) ;
269204
270- // Handle initial hash if present
271205 if ( location . hash ) {
206+ const targetId = location . hash . substring ( 1 ) ;
207+ const targetEl = document . getElementById ( targetId ) ;
208+
272209 handleHashNavigation ( location . hash ) ;
210+
211+ if ( targetEl ) {
212+ setTimeout ( ( ) => {
213+ const y = targetEl . getBoundingClientRect ( ) . top + window . scrollY - 280 ;
214+ window . scrollTo ( { top : y , behavior : 'smooth' } ) ;
215+ } , 150 ) ;
216+ }
273217 }
274218 } ) ;
275219 } ) ;
276220
277- // Set up browser back/forward navigation handler
221+ // For popstate, we DO want to scroll since DetailsClicksClient isn't involved
278222 window . addEventListener ( 'popstate' , function ( ) {
279- handleHashNavigation ( location . hash ) ;
223+ const hash = location . hash ;
224+ if ( ! hash ) return ;
225+
226+ const targetId = hash . substring ( 1 ) ;
227+ const targetEl = document . getElementById ( targetId ) ;
228+
229+ handleHashNavigation ( hash ) ;
230+
231+ if ( targetEl ) {
232+ setTimeout ( ( ) => {
233+ const y = targetEl . getBoundingClientRect ( ) . top + window . scrollY - 280 ;
234+ window . scrollTo ( { top : y , behavior : 'smooth' } ) ;
235+ } , 150 ) ;
236+ }
280237 } ) ;
281238
282- // Set up hashchange handler (triggered by internal navigation)
283- window . addEventListener ( 'hashchange' , function ( ) {
284- highlightActiveOnPageLink ( ) ;
285- handleHashNavigation ( location . hash ) ;
239+ // NOTE: hashchange only handles highlighting, NOT scrolling (to avoid race condition)
240+ // Scrolling is handled by DetailsClicksClient's universal click handler
241+ window . addEventListener ( 'hashchange' , function ( e ) {
242+ const newHash = e . newURL ? e . newURL . split ( '#' ) [ 1 ] : '' ;
243+ const hashToUse = newHash ? '#' + newHash : location . hash ;
244+
245+ if ( ! hashToUse ) {
246+ return ;
247+ }
248+
249+ const targetId = hashToUse . substring ( 1 ) ;
250+ highlightActiveOnPageLink ( hashToUse ) ;
251+ highlightDetailsOnActiveHash ( targetId ) ;
286252 } ) ;
287253
288254 return ;
289255 }
290256
291- // Handle hash changes on subsequent navigations
292- if ( location . hash !== previousHash ) {
293- handleHashNavigation ( location . hash ) ;
294- previousHash = location . hash ;
295- }
296-
297- // Re-initialize event handlers for newly rendered elements
257+ // Hash changes on subsequent navigations are handled by:
258+ // - hashchange listener (for browser back/forward, URL bar edits)
259+ // - DetailsClicksClient click handler (for link clicks)
260+ previousHash = location . hash ;
298261 initializeEventHandlers ( ) ;
299262}
0 commit comments