@@ -4,10 +4,11 @@ import type { Header } from '../../shared'
4
4
import { useAside } from './aside'
5
5
import { throttleAndDebounce } from '../support/utils'
6
6
7
- // magic number to avoid repeated retrieval
8
- const PAGE_OFFSET = 71
7
+ // cached list of anchor elements from resolveHeaders
8
+ const resolvedHeaders : { element : HTMLHeadElement ; link : string } [ ] = [ ]
9
9
10
10
export type MenuItem = Omit < Header , 'slug' | 'children' > & {
11
+ element : HTMLHeadElement
11
12
children ?: MenuItem [ ]
12
13
}
13
14
@@ -29,6 +30,7 @@ export function getHeaders(range: DefaultTheme.Config['outline']) {
29
30
. map ( ( el ) => {
30
31
const level = Number ( el . tagName [ 1 ] )
31
32
return {
33
+ element : el as HTMLHeadElement ,
32
34
title : serializeHeader ( el ) ,
33
35
link : '#' + el . id ,
34
36
level
@@ -78,6 +80,12 @@ export function resolveHeaders(
78
80
: levelsRange
79
81
80
82
headers = headers . filter ( ( h ) => h . level >= high && h . level <= low )
83
+ // clear previous caches
84
+ resolvedHeaders . length = 0
85
+ // update global header list for active link rendering
86
+ for ( const { element, link } of headers ) {
87
+ resolvedHeaders . push ( { element, link } )
88
+ }
81
89
82
90
const ret : MenuItem [ ] = [ ]
83
91
outer: for ( let i = 0 ; i < headers . length ; i ++ ) {
@@ -128,40 +136,55 @@ export function useActiveAnchor(
128
136
return
129
137
}
130
138
131
- const links = [ ] . slice . call (
132
- container . value . querySelectorAll ( '.outline-link' )
133
- ) as HTMLAnchorElement [ ]
134
-
135
- const anchors = [ ] . slice
136
- . call ( document . querySelectorAll ( '.content .header-anchor' ) )
137
- . filter ( ( anchor : HTMLAnchorElement ) => {
138
- return links . some ( ( link ) => {
139
- return link . hash === anchor . hash && anchor . offsetParent !== null
140
- } )
141
- } ) as HTMLAnchorElement [ ]
139
+ // pixel offset, start of main content
140
+ const offsetDocTop = ( ( ) => {
141
+ const container =
142
+ document . querySelector ( '#VPContent .VPDoc' ) ?. firstElementChild
143
+ if ( container ) return getAbsoluteTop ( container as HTMLElement )
144
+ else return 78
145
+ } ) ( )
142
146
143
147
const scrollY = window . scrollY
144
148
const innerHeight = window . innerHeight
145
149
const offsetHeight = document . body . offsetHeight
146
150
const isBottom = Math . abs ( scrollY + innerHeight - offsetHeight ) < 1
147
151
148
- // page bottom - highlight last one
149
- if ( anchors . length && isBottom ) {
150
- activateLink ( anchors [ anchors . length - 1 ] . hash )
152
+ // resolvedHeaders may be repositioned, hidden or fix positioned
153
+ const headers = resolvedHeaders
154
+ . map ( ( { element, link } ) => ( {
155
+ link,
156
+ top : getAbsoluteTop ( element )
157
+ } ) )
158
+ . filter ( ( { top } ) => ! Number . isNaN ( top ) )
159
+ . sort ( ( a , b ) => a . top - b . top )
160
+
161
+ // no headers available for active link
162
+ if ( ! headers . length ) {
163
+ activateLink ( null )
151
164
return
152
165
}
153
166
154
- for ( let i = 0 ; i < anchors . length ; i ++ ) {
155
- const anchor = anchors [ i ]
156
- const nextAnchor = anchors [ i + 1 ]
167
+ // page top
168
+ if ( scrollY < 1 ) {
169
+ activateLink ( null )
170
+ return
171
+ }
157
172
158
- const [ isActive , hash ] = isAnchorActive ( i , anchor , nextAnchor )
173
+ // page bottom - highlight last link
174
+ if ( isBottom ) {
175
+ activateLink ( headers [ headers . length - 1 ] . link )
176
+ return
177
+ }
159
178
160
- if ( isActive ) {
161
- activateLink ( hash )
162
- return
179
+ // find the last header above the top of viewport
180
+ let activeLink : string | null = null
181
+ for ( const { link, top } of headers ) {
182
+ if ( top > scrollY + offsetDocTop ) {
183
+ break
163
184
}
185
+ activeLink = link
164
186
}
187
+ activateLink ( activeLink )
165
188
}
166
189
167
190
function activateLink ( hash : string | null ) {
@@ -190,28 +213,18 @@ export function useActiveAnchor(
190
213
}
191
214
}
192
215
193
- function getAnchorTop ( anchor : HTMLAnchorElement ) : number {
194
- return anchor . parentElement ! . offsetTop - PAGE_OFFSET
195
- }
196
-
197
- function isAnchorActive (
198
- index : number ,
199
- anchor : HTMLAnchorElement ,
200
- nextAnchor : HTMLAnchorElement | undefined
201
- ) : [ boolean , string | null ] {
202
- const scrollTop = window . scrollY
203
-
204
- if ( index === 0 && scrollTop === 0 ) {
205
- return [ true , null ]
206
- }
207
-
208
- if ( scrollTop < getAnchorTop ( anchor ) ) {
209
- return [ false , null ]
210
- }
211
-
212
- if ( ! nextAnchor || scrollTop < getAnchorTop ( nextAnchor ) ) {
213
- return [ true , anchor . hash ]
216
+ function getAbsoluteTop ( element : HTMLElement ) : number {
217
+ let offsetTop = 0
218
+ while ( element !== document . body ) {
219
+ if ( element === null ) {
220
+ // child element is:
221
+ // - not attached to the DOM (display: none)
222
+ // - set to fixed position (not scrollable)
223
+ // - body or html element (null offsetParent)
224
+ return NaN
225
+ }
226
+ offsetTop += element . offsetTop
227
+ element = element . offsetParent as HTMLElement
214
228
}
215
-
216
- return [ false , null ]
229
+ return offsetTop
217
230
}
0 commit comments