11import {
22 ref ,
3- Ref ,
43 onMounted ,
54 computed ,
65 unref ,
76 watch ,
87 isRef ,
98 isReactive ,
109 onBeforeUnmount ,
10+ reactive ,
11+ type Ref ,
1112} from 'vue' ;
1213import { useScroll } from './useScroll' ;
13- import { getEdges , getRects , useRestrictedRef , isSSR , FIXED_TO_TOP_OFFSET } from './utils' ;
14+ import { getEdges , useMediaRef , isSSR , FIXED_OFFSET } from './utils' ;
1415
1516type UseActiveTitleOptions = {
1617 jumpToFirst ?: boolean ;
@@ -67,19 +68,26 @@ export function useActive(
6768 const media = `(min-width: ${ minWidth } px)` ;
6869
6970 // Internal
70- const root = ref < HTMLElement | null > ( null ) ;
71- const rootTop = ref ( 0 ) ;
72- const targets = ref < HTMLElement [ ] > ( [ ] ) ;
7371 const matchMedia = ref ( isSSR || window . matchMedia ( media ) . matches ) ;
72+ const root = ref < HTMLElement | null > ( null ) ;
73+ const targets = reactive ( {
74+ elements : [ ] as HTMLElement [ ] ,
75+ top : new Map < string , number > ( ) ,
76+ bottom : new Map < string , number > ( ) ,
77+ } ) ;
7478
7579 const isHTML = computed ( ( ) => typeof rootId !== 'string' ) ;
76- const ids = computed ( ( ) => targets . value . map ( ( { id } ) => id ) ) ;
80+ const ids = computed ( ( ) => targets . elements . map ( ( { id } ) => id ) ) ;
7781
7882 // Returned values
79- const activeId = useRestrictedRef ( matchMedia , '' ) ;
83+ const activeId = useMediaRef ( matchMedia , '' ) ;
8084 const activeIndex = computed ( ( ) => ids . value . indexOf ( activeId . value ) ) ;
8185
82- // Runs onMount and whenever the user array changes
86+ function getTop ( ) {
87+ return root . value ! . getBoundingClientRect ( ) . top - ( isHTML . value ? 0 : root . value ! . scrollTop ) ;
88+ }
89+
90+ // Runs onMount, onResize and whenever the user array changes
8391 function setTargets ( ) {
8492 let _targets = < HTMLElement [ ] > [ ] ;
8593
@@ -91,7 +99,14 @@ export function useActive(
9199 } ) ;
92100
93101 _targets . sort ( ( a , b ) => a . offsetTop - b . offsetTop ) ;
94- targets . value = _targets ;
102+ targets . elements = _targets ;
103+
104+ const rootTop = getTop ( ) ;
105+ targets . elements . forEach ( ( target ) => {
106+ const { top, bottom } = target . getBoundingClientRect ( ) ;
107+ targets . top . set ( target . id , top - rootTop ) ;
108+ targets . bottom . set ( target . id , bottom - rootTop ) ;
109+ } ) ;
95110 }
96111
97112 function jumpToEdges ( ) {
@@ -123,31 +138,50 @@ export function useActive(
123138 }
124139 }
125140
141+ function getSentinel ( ) {
142+ return isHTML . value ? getTop ( ) : - root . value ! . scrollTop ;
143+ }
144+
126145 // Sets first target that LEFT the top
127146 function onScrollDown ( { isCancel } = { isCancel : false } ) {
128- // overlayHeight not needed as 'scroll-margin-top' is set instead
129- const offset = rootTop . value + toBottom ! ;
130- const outTargets = Array . from ( getRects ( targets . value , 'OUT' , offset ) . keys ( ) ) ;
131- const firstOut = outTargets [ outTargets . length - 1 ] ?? ( jumpToFirst ? ids . value [ 0 ] : '' ) ;
147+ let firstOut = jumpToFirst ? ids . value [ 0 ] : '' ;
148+
149+ const sentinel = getSentinel ( ) ;
150+ const offset = FIXED_OFFSET + overlayHeight + toBottom ! ;
151+
152+ Array . from ( targets . top ) . some ( ( [ id , top ] ) => {
153+ if ( sentinel + top < offset ) {
154+ return ( firstOut = id ) , false ;
155+ }
156+ return true ; // Return last
157+ } ) ;
132158
133- // Prevent innatural highlighting with smoothscroll/custom easings
159+ // Prevent innatural highlighting with smoothscroll/custom easings...
134160 if ( ids . value . indexOf ( firstOut ) > ids . value . indexOf ( activeId . value ) ) {
135161 return ( activeId . value = firstOut ) ;
136162 }
137163
164+ // ...but not on scroll cancel
138165 if ( isCancel ) {
139166 activeId . value = firstOut ;
140167 }
141168 }
142169
143170 // Sets first target that ENTERED the top
144171 function onScrollUp ( ) {
145- const offset = overlayHeight + rootTop . value + toTop ! ;
146- const firstIn = getRects ( targets . value , 'IN' , offset ) . keys ( ) . next ( ) . value ?? '' ;
172+ let firstIn = '' ;
173+
174+ const sentinel = getSentinel ( ) ;
175+ const offset = FIXED_OFFSET + overlayHeight + toTop ! ;
176+
177+ Array . from ( targets . bottom ) . some ( ( [ id , bottom ] ) => {
178+ if ( sentinel + bottom > offset ) {
179+ return ( firstIn = id ) , true ; // Return first
180+ }
181+ } ) ;
147182
148183 if ( ! jumpToFirst && firstIn === ids . value [ 0 ] ) {
149- const firstTargetTop = getRects ( targets . value , 'ALL' ) . values ( ) . next ( ) . value ;
150- if ( firstTargetTop > FIXED_TO_TOP_OFFSET + offset ) {
184+ if ( sentinel + targets . top . values ( ) . next ( ) . value > offset ) {
151185 return ( activeId . value = '' ) ;
152186 }
153187 }
@@ -158,29 +192,24 @@ export function useActive(
158192 }
159193
160194 function onResize ( ) {
161- rootTop . value = isHTML . value ? 0 : root . value ! . getBoundingClientRect ( ) . top ;
162195 matchMedia . value = window . matchMedia ( media ) . matches ;
196+ setTargets ( ) ;
163197 }
164198
165199 onMounted ( async ( ) => {
166- if ( isHTML . value ) {
167- root . value = document . documentElement ;
168- } else {
169- const cRoot = document . getElementById ( rootId as string ) ;
170- if ( cRoot ) {
171- root . value = cRoot ;
172- rootTop . value = cRoot . getBoundingClientRect ( ) . top ;
173- }
174- }
200+ root . value = isHTML . value
201+ ? document . documentElement
202+ : document . getElementById ( rootId as string ) ;
175203
176204 // https://github.com/nuxt/content/issues/1799
177205 await new Promise ( ( resolve ) => setTimeout ( resolve ) ) ;
206+
178207 setTargets ( ) ;
179208
180209 window . addEventListener ( 'resize' , onResize , { passive : true } ) ;
181210
182211 if ( matchMedia . value ) {
183- const hashId = targets . value . find ( ( { id } ) => id === location . hash . slice ( 1 ) ) ?. id ;
212+ const hashId = targets . elements . find ( ( { id } ) => id === location . hash . slice ( 1 ) ) ?. id ;
184213 if ( hashId ) {
185214 return ( activeId . value = hashId ) ;
186215 }
0 commit comments