11import type { TocItem } from '#shared/types/readme'
22import type { Ref } from 'vue'
3- import { scrollToAnchor } from '~/utils/scrollToAnchor'
43
54/**
65 * Composable for tracking the currently visible heading in a TOC.
76 * Uses IntersectionObserver to detect which heading is at the top of the viewport.
87 *
98 * @param toc - Reactive array of TOC items
10- * @returns Object containing activeId and scrollToHeading function
9+ * @returns Object containing activeId
1110 * @public
1211 */
1312export function useActiveTocItem ( toc : Ref < TocItem [ ] > ) {
@@ -16,12 +15,11 @@ export function useActiveTocItem(toc: Ref<TocItem[]>) {
1615 // Only run observer logic on client
1716 if ( import . meta. server ) {
1817 // eslint-disable-next-line @typescript-eslint/no-empty-function
19- return { activeId, scrollToHeading : ( _id : string ) => { } }
18+ return { activeId }
2019 }
2120
2221 let observer : IntersectionObserver | null = null
2322 const headingElements = new Map < string , Element > ( )
24- let scrollCleanup : ( ( ) => void ) | null = null
2523
2624 const setupObserver = ( ) => {
2725 // Clean up previous observer
@@ -92,84 +90,6 @@ export function useActiveTocItem(toc: Ref<TocItem[]>) {
9290 }
9391 }
9492
95- // Scroll to a heading with observer disconnection during scroll
96- const scrollToHeading = ( id : string ) => {
97- if ( ! document . getElementById ( id ) ) return
98-
99- // Clean up any previous scroll monitoring
100- if ( scrollCleanup ) {
101- scrollCleanup ( )
102- scrollCleanup = null
103- }
104-
105- // Immediately set activeId
106- activeId . value = id
107-
108- // Disconnect observer to prevent interference during scroll
109- if ( observer ) {
110- observer . disconnect ( )
111- }
112-
113- // Scroll, but do not update url until scroll ends
114- scrollToAnchor ( id , { updateUrl : false } )
115-
116- const handleScrollEnd = ( ) => {
117- history . replaceState ( null , '' , `#${ id } ` )
118- setupObserver ( )
119- scrollCleanup = null
120- }
121-
122- // Check for scrollend support (Chrome 114+, Firefox 109+, Safari 18+)
123- const supportsScrollEnd = 'onscrollend' in window
124-
125- if ( supportsScrollEnd ) {
126- window . addEventListener ( 'scrollend' , handleScrollEnd , { once : true } )
127- scrollCleanup = ( ) => window . removeEventListener ( 'scrollend' , handleScrollEnd )
128- } else {
129- // Fallback: use RAF polling for older browsers
130- let lastScrollY = window . scrollY
131- let stableFrames = 0
132- let rafId : number | null = null
133- const STABLE_THRESHOLD = 5 // Number of frames with no movement to consider settled
134-
135- const checkScrollSettled = ( ) => {
136- const currentScrollY = window . scrollY
137-
138- if ( Math . abs ( currentScrollY - lastScrollY ) < 1 ) {
139- stableFrames ++
140- if ( stableFrames >= STABLE_THRESHOLD ) {
141- handleScrollEnd ( )
142- return
143- }
144- } else {
145- stableFrames = 0
146- }
147-
148- lastScrollY = currentScrollY
149- rafId = requestAnimationFrame ( checkScrollSettled )
150- }
151-
152- rafId = requestAnimationFrame ( checkScrollSettled )
153-
154- scrollCleanup = ( ) => {
155- if ( rafId !== null ) {
156- cancelAnimationFrame ( rafId )
157- rafId = null
158- }
159- }
160- }
161-
162- // Safety timeout - reconnect observer after max scroll time
163- setTimeout ( ( ) => {
164- if ( scrollCleanup ) {
165- scrollCleanup ( )
166- scrollCleanup = null
167- history . replaceState ( null , '' , `#${ id } ` )
168- setupObserver ( )
169- }
170- } , 1000 )
171- }
172-
17393 // Set up observer when TOC changes
17494 watch (
17595 toc ,
@@ -182,15 +102,11 @@ export function useActiveTocItem(toc: Ref<TocItem[]>) {
182102
183103 // Clean up on unmount
184104 onUnmounted ( ( ) => {
185- if ( scrollCleanup ) {
186- scrollCleanup ( )
187- scrollCleanup = null
188- }
189105 if ( observer ) {
190106 observer . disconnect ( )
191107 observer = null
192108 }
193109 } )
194110
195- return { activeId, scrollToHeading }
111+ return { activeId }
196112}
0 commit comments