import { throttle } from 'lodash' ; import { useCallback , useEffect , useState } from 'react' ; import { TOCHeader } from '@/context/jupyterBook' ; /**
 * The designs requires a 35px margin between the heading and the top of the
 * viewport when scrolling to a heading, so the next heading should be
 * highlighted when hitting that offset.
 */ const TOP_OFFSET_PX = 35 ; /**
 * Duration of throttling for  `findActiveHeader()` function. This is so that
 * the `findActiveHeader()` function is called no more than 100ms so that we can
 * reduce expensive calls to `getBoundingClientRect()`.
 */ const FIND_ACTIVE_HEADER_THROTTLE_MS = 100 ; /**
 * Duration to wait for before setting the active header on initial load.
 */ const SET_INITIAL_HEADER_TIMEOUT_MS = 200 ; param value returns function removeLeadingHash ( value ) { return value . replace ( /^#/ , '' ) ; } param headers returns function firstHeaderInViewport ( headers ) { let firstHeaderTop = 0 ; let firstHeaderIndex = - 1 ; for ( let i = 0 ; i < headers . length ; i += 1 ) { const id = removeLeadingHash ( headers [ i ] . href ) ; const node = document . getElementById ( id ) ; const top = node ?. getBoundingClientRect ( ) ?. top ?? 0 ; if ( top >= 0 ) { firstHeaderTop = top ; firstHeaderIndex = i ; break ; } } const firstHeader = headers [ firstHeaderIndex ] as TOCHeader | undefined ; return { firstHeader , firstHeaderTop , firstHeaderIndex , } ; } interface UseActiveHeaderOptions { /**
     * Enable effect for finding the active header.
     */ enabled ? : boolean ; /**
     * Headers rendered on the current page.
     */ headers : TOCHeader [ ] ; } param headers returns export function useActiveHeader ( { enabled = true , headers , } ) { const [ activeHeader , setActiveHeader ] = useState ( headers [ 0 ] ?. href ?? '' ) ; const findActiveHeader = useCallback ( throttle ( ( ) => { const { firstHeader , firstHeaderIndex , firstHeaderTop } = firstHeaderInViewport ( headers ) ; if ( ! firstHeader ) { return ; } if ( Math . floor ( firstHeaderTop ) <= TOP_OFFSET_PX ) { setActiveHeader ( firstHeader . href ) ; } else { const previousHeader = headers [ firstHeaderIndex - 1 ] ?? firstHeader ; setActiveHeader ( previousHeader . href ) ; } } , FIND_ACTIVE_HEADER_THROTTLE_MS ) , [ headers ] ) ; const enableEventHandlers = useCallback ( ( ) => { document . addEventListener ( 'scroll' , findActiveHeader ) ; document . addEventListener ( 'resize' , findActiveHeader ) ; } , [ findActiveHeader ] ) ; const disableEventHandlers = useCallback ( ( ) => { document . removeEventListener ( 'scroll' , findActiveHeader ) ; document . removeEventListener ( 'resize' , findActiveHeader ) ; } , [ findActiveHeader ] ) ; useEffect ( ( ) => { if ( ! enabled ) { return ( ) => { } ; } const initialHeader = window . location . hash ; if ( headers . find ( ( header ) => header . href === initialHeader ) ) { setTimeout ( ( ) => { if ( window . innerHeight + window . scrollY >= document . body . scrollHeight ) { setActiveHeader ( initialHeader ) ; } else { findActiveHeader ( ) ; } enableEventHandlers ( ) ; } , SET_INITIAL_HEADER_TIMEOUT_MS ) ; } else { findActiveHeader ( ) ; enableEventHandlers ( ) ; } return disableEventHandlers ; } , [ disableEventHandlers , enableEventHandlers , enabled , findActiveHeader , headers , ] ) ; return { activeHeader , setActiveHeader , enableEventHandlers , disableEventHandlers , } ; }