@@ -3,86 +3,124 @@ import { onMounted, onUnmounted, onUpdated } from 'vue'
33export function useActiveSidebarLinks ( ) {
44 let rootActiveLink : HTMLAnchorElement | null = null
55 let activeLink : HTMLAnchorElement | null = null
6- const decode = decodeURIComponent
76
8- const deactiveLink = ( link : HTMLAnchorElement | null ) =>
9- link && link . classList . remove ( 'active' )
10-
11- const activateLink = ( hash : string ) => {
12- deactiveLink ( activeLink )
13- deactiveLink ( rootActiveLink )
14- activeLink = document . querySelector ( `.sidebar a[href="${ hash } "]` )
15- if ( activeLink ) {
16- activeLink . classList . add ( 'active' )
17- // also add active class to parent h2 anchors
18- const rootLi = activeLink . closest ( '.sidebar > ul > li' )
19- if ( rootLi && rootLi !== activeLink . parentElement ) {
20- rootActiveLink = rootLi . querySelector ( 'a' )
21- rootActiveLink && rootActiveLink . classList . add ( 'active' )
22- } else {
23- rootActiveLink = null
24- }
25- }
26- }
27-
28- const setActiveLink = ( ) => {
29- const sidebarLinks = [ ] . slice . call (
30- document . querySelectorAll ( '.sidebar a' )
31- ) as HTMLAnchorElement [ ]
32-
33- const anchors = [ ] . slice
34- . call ( document . querySelectorAll ( '.header-anchor' ) )
35- . filter ( ( anchor : HTMLAnchorElement ) =>
36- sidebarLinks . some ( ( sidebarLink ) => sidebarLink . hash === anchor . hash )
37- ) as HTMLAnchorElement [ ]
38-
39- const pageOffset = ( document . querySelector ( '.navbar' ) as HTMLElement )
40- . offsetHeight
41- const scrollTop = window . scrollY
7+ const onScroll = throttleAndDebounce ( setActiveLink , 300 )
428
43- const getAnchorTop = ( anchor : HTMLAnchorElement ) : number =>
44- anchor . parentElement ! . offsetTop - pageOffset - 15
9+ function setActiveLink ( ) : void {
10+ const sidebarLinks = getSidebarLinks ( )
11+ const anchors = getAnchors ( sidebarLinks )
4512
4613 for ( let i = 0 ; i < anchors . length ; i ++ ) {
4714 const anchor = anchors [ i ]
4815 const nextAnchor = anchors [ i + 1 ]
49- const isActive =
50- ( i === 0 && scrollTop === 0 ) ||
51- ( scrollTop >= getAnchorTop ( anchor ) &&
52- ( ! nextAnchor || scrollTop < getAnchorTop ( nextAnchor ) ) )
5316
54- // TODO: fix case when at page bottom
17+ const [ isActive , hash ] = isAnchorActive ( i , anchor , nextAnchor )
5518
5619 if ( isActive ) {
57- const targetHash = decode ( anchor . hash )
58- history . replaceState ( null , document . title , targetHash )
59- activateLink ( targetHash )
20+ history . replaceState ( null , document . title , hash ? hash : ' ' )
21+ activateLink ( hash )
6022 return
6123 }
6224 }
6325 }
6426
65- const onScroll = throttleAndDebounce ( setActiveLink , 300 )
27+ function activateLink ( hash : string | null ) : void {
28+ deactiveLink ( activeLink )
29+ deactiveLink ( rootActiveLink )
30+
31+ activeLink = document . querySelector ( `.sidebar a[href="${ hash } "]` )
32+
33+ if ( ! activeLink ) {
34+ return
35+ }
36+
37+ activeLink . classList . add ( 'active' )
38+
39+ // also add active class to parent h2 anchors
40+ const rootLi = activeLink . closest ( '.sidebar-links > ul > li' )
41+
42+ if ( rootLi && rootLi !== activeLink . parentElement ) {
43+ rootActiveLink = rootLi . querySelector ( 'a' )
44+ rootActiveLink && rootActiveLink . classList . add ( 'active' )
45+ } else {
46+ rootActiveLink = null
47+ }
48+ }
49+
50+ function deactiveLink ( link : HTMLAnchorElement | null ) : void {
51+ link && link . classList . remove ( 'active' )
52+ }
53+
6654 onMounted ( ( ) => {
6755 setActiveLink ( )
6856 window . addEventListener ( 'scroll' , onScroll )
6957 } )
7058
7159 onUpdated ( ( ) => {
7260 // sidebar update means a route change
73- activateLink ( decode ( location . hash ) )
61+ activateLink ( decodeURIComponent ( location . hash ) )
7462 } )
7563
7664 onUnmounted ( ( ) => {
7765 window . removeEventListener ( 'scroll' , onScroll )
7866 } )
7967}
8068
69+ function getSidebarLinks ( ) : HTMLAnchorElement [ ] {
70+ return [ ] . slice . call (
71+ document . querySelectorAll ( '.sidebar a.sidebar-link-item' )
72+ )
73+ }
74+
75+ function getAnchors ( sidebarLinks : HTMLAnchorElement [ ] ) : HTMLAnchorElement [ ] {
76+ return [ ] . slice
77+ . call ( document . querySelectorAll ( '.header-anchor' ) )
78+ . filter ( ( anchor : HTMLAnchorElement ) =>
79+ sidebarLinks . some ( ( sidebarLink ) => sidebarLink . hash === anchor . hash )
80+ ) as HTMLAnchorElement [ ]
81+ }
82+
83+ function getPageOffset ( ) : number {
84+ return ( document . querySelector ( '.navbar' ) as HTMLElement ) . offsetHeight
85+ }
86+
87+ function getAnchorTop ( anchor : HTMLAnchorElement ) : number {
88+ const pageOffset = getPageOffset ( )
89+
90+ return anchor . parentElement ! . offsetTop - pageOffset - 15
91+ }
92+
93+ function isAnchorActive (
94+ index : number ,
95+ anchor : HTMLAnchorElement ,
96+ nextAnchor : HTMLAnchorElement
97+ ) : [ boolean , string | null ] {
98+ const scrollTop = window . scrollY
99+
100+ if ( index === 0 && scrollTop === 0 ) {
101+ return [ true , null ]
102+ }
103+
104+ if ( scrollTop < getAnchorTop ( anchor ) ) {
105+ return [ false , null ]
106+ }
107+
108+ if ( ! nextAnchor || scrollTop < getAnchorTop ( nextAnchor ) ) {
109+ return [ true , decodeURIComponent ( anchor . hash ) ]
110+ }
111+
112+ return [ false , null ]
113+ }
114+
81115function throttleAndDebounce ( fn : ( ) => void , delay : number ) : ( ) => void {
82116 let timeout : NodeJS . Timeout
83117 let called = false
118+
84119 return ( ) => {
85- if ( timeout ) clearTimeout ( timeout )
120+ if ( timeout ) {
121+ clearTimeout ( timeout )
122+ }
123+
86124 if ( ! called ) {
87125 fn ( )
88126 called = true
0 commit comments