@@ -7,6 +7,8 @@ import { getScrollOffset, inBrowser, withBase } from './utils'
77
88export interface Route {
99 path : string
10+ hash : string
11+ query : string
1012 data : PageData
1113 component : Component | null
1214}
@@ -19,7 +21,15 @@ export interface Router {
1921 /**
2022 * Navigate to a new URL.
2123 */
22- go : ( to ?: string ) => Promise < void >
24+ go : (
25+ to : string ,
26+ options ?: {
27+ // @internal
28+ initialLoad ?: boolean
29+ // Whether to smoothly scroll to the target position.
30+ smoothScroll ?: boolean
31+ }
32+ ) => Promise < void >
2333 /**
2434 * Called before the route changes. Return `false` to cancel the navigation.
2535 */
@@ -37,10 +47,6 @@ export interface Router {
3747 * Called after the route changes.
3848 */
3949 onAfterRouteChange ?: ( to : string ) => Awaitable < void >
40- /**
41- * @deprecated use `onAfterRouteChange` instead
42- */
43- onAfterRouteChanged ?: ( to : string ) => Awaitable < void >
4450}
4551
4652export const RouterSymbol : InjectionKey < Router > = Symbol ( )
@@ -51,6 +57,8 @@ const fakeHost = 'http://a.com'
5157
5258const getDefaultRoute = ( ) : Route => ( {
5359 path : '/' ,
60+ hash : '' ,
61+ query : '' ,
5462 component : null ,
5563 data : notFoundPageData
5664} )
@@ -68,39 +76,32 @@ export function createRouter(
6876
6977 const router : Router = {
7078 route,
71- go
72- }
73-
74- async function go ( href : string = inBrowser ? location . href : '/' ) {
75- href = normalizeHref ( href )
76- if ( ( await router . onBeforeRouteChange ?.( href ) ) === false ) return
77- if ( inBrowser && href !== normalizeHref ( location . href ) ) {
78- // save scroll position before changing url
79- history . replaceState ( { scrollPosition : window . scrollY } , '' )
80- history . pushState ( { } , '' , href )
79+ async go ( href , options ) {
80+ href = normalizeHref ( href )
81+ if ( ( await router . onBeforeRouteChange ?.( href ) ) === false ) return
82+ if ( ! inBrowser || ( await changeRoute ( href , options ) ) ) await loadPage ( href )
83+ syncRouteQueryAndHash ( )
84+ await router . onAfterRouteChange ?.( href )
8185 }
82- await loadPage ( href )
83- await ( router . onAfterRouteChange ?? router . onAfterRouteChanged ) ?.( href )
8486 }
8587
8688 let latestPendingPath : string | null = null
8789
8890 async function loadPage ( href : string , scrollPosition = 0 , isRetry = false ) {
8991 if ( ( await router . onBeforePageLoad ?.( href ) ) === false ) return
92+
9093 const targetLoc = new URL ( href , fakeHost )
9194 const pendingPath = ( latestPendingPath = targetLoc . pathname )
95+
9296 try {
9397 let page = await loadPageModule ( pendingPath )
94- if ( ! page ) {
95- throw new Error ( `Page not found: ${ pendingPath } ` )
96- }
98+ if ( ! page ) throw new Error ( `Page not found: ${ pendingPath } ` )
99+
97100 if ( latestPendingPath === pendingPath ) {
98101 latestPendingPath = null
99102
100103 const { default : comp , __pageData } = page
101- if ( ! comp ) {
102- throw new Error ( `Invalid route component: ${ comp } ` )
103- }
104+ if ( ! comp ) throw new Error ( `Invalid route component: ${ comp } ` )
104105
105106 await router . onAfterPageLoad ?.( href )
106107
@@ -109,36 +110,25 @@ export function createRouter(
109110 route . data = import . meta. env . PROD
110111 ? markRaw ( __pageData )
111112 : ( readonly ( __pageData ) as PageData )
113+ syncRouteQueryAndHash ( targetLoc )
112114
113115 if ( inBrowser ) {
114116 nextTick ( ( ) => {
115117 let actualPathname =
116118 siteDataRef . value . base +
117119 __pageData . relativePath . replace ( / (?: ( ^ | \/ ) i n d e x ) ? \. m d $ / , '$1' )
120+
118121 if ( ! siteDataRef . value . cleanUrls && ! actualPathname . endsWith ( '/' ) ) {
119122 actualPathname += '.html'
120123 }
124+
121125 if ( actualPathname !== targetLoc . pathname ) {
122126 targetLoc . pathname = actualPathname
123127 href = actualPathname + targetLoc . search + targetLoc . hash
124128 history . replaceState ( { } , '' , href )
125129 }
126130
127- if ( targetLoc . hash && ! scrollPosition ) {
128- let target : HTMLElement | null = null
129- try {
130- target = document . getElementById (
131- decodeURIComponent ( targetLoc . hash ) . slice ( 1 )
132- )
133- } catch ( e ) {
134- console . warn ( e )
135- }
136- if ( target ) {
137- scrollTo ( target , targetLoc . hash )
138- return
139- }
140- }
141- window . scrollTo ( 0 , scrollPosition )
131+ return scrollTo ( targetLoc . hash , false , scrollPosition )
142132 } )
143133 }
144134 }
@@ -173,14 +163,22 @@ export function createRouter(
173163 . replace ( / ^ \/ / , '' )
174164 : '404.md'
175165 route . data = { ...notFoundPageData , relativePath }
166+ syncRouteQueryAndHash ( targetLoc )
176167 }
177168 }
178169 }
179170
171+ function syncRouteQueryAndHash (
172+ loc : { search : string ; hash : string } = inBrowser
173+ ? location
174+ : { search : '' , hash : '' }
175+ ) {
176+ route . query = loc . search
177+ route . hash = decodeURIComponent ( loc . hash )
178+ }
179+
180180 if ( inBrowser ) {
181- if ( history . state === null ) {
182- history . replaceState ( { } , '' )
183- }
181+ if ( history . state === null ) history . replaceState ( { } , '' )
184182 window . addEventListener (
185183 'click' ,
186184 ( e ) => {
@@ -193,56 +191,34 @@ export function createRouter(
193191 e . shiftKey ||
194192 e . altKey ||
195193 e . metaKey
196- )
194+ ) {
197195 return
196+ }
198197
199198 const link = e . target . closest < HTMLAnchorElement | SVGAElement > ( 'a' )
200199 if (
201200 ! link ||
202201 link . closest ( '.vp-raw' ) ||
203202 link . hasAttribute ( 'download' ) ||
204203 link . hasAttribute ( 'target' )
205- )
204+ ) {
206205 return
206+ }
207207
208208 const linkHref =
209209 link . getAttribute ( 'href' ) ??
210210 ( link instanceof SVGAElement ? link . getAttribute ( 'xlink:href' ) : null )
211211 if ( linkHref == null ) return
212212
213- const { href, origin, pathname, hash, search } = new URL (
214- linkHref ,
215- link . baseURI
216- )
217- const currentUrl = new URL ( location . href ) // copy to keep old data
213+ const { href, origin, pathname } = new URL ( linkHref , link . baseURI )
214+ const currentLoc = new URL ( location . href ) // copy to keep old data
218215 // only intercept inbound html links
219- if ( origin === currentUrl . origin && treatAsHtml ( pathname ) ) {
216+ if ( origin === currentLoc . origin && treatAsHtml ( pathname ) ) {
220217 e . preventDefault ( )
221- if (
222- pathname === currentUrl . pathname &&
223- search === currentUrl . search
224- ) {
225- // scroll between hash anchors in the same page
226- // avoid duplicate history entries when the hash is same
227- if ( hash !== currentUrl . hash ) {
228- history . pushState ( { } , '' , href )
229- // still emit the event so we can listen to it in themes
230- window . dispatchEvent (
231- new HashChangeEvent ( 'hashchange' , {
232- oldURL : currentUrl . href ,
233- newURL : href
234- } )
235- )
236- }
237- if ( hash ) {
238- // use smooth scroll when clicking on header anchor links
239- scrollTo ( link , hash , link . classList . contains ( 'header-anchor' ) )
240- } else {
241- window . scrollTo ( 0 , 0 )
242- }
243- } else {
244- go ( href )
245- }
218+ router . go ( href , {
219+ // use smooth scroll when clicking on header anchor links
220+ smoothScroll : link . classList . contains ( 'header-anchor' )
221+ } )
246222 }
247223 } ,
248224 { capture : true }
@@ -252,11 +228,13 @@ export function createRouter(
252228 if ( e . state === null ) return
253229 const href = normalizeHref ( location . href )
254230 await loadPage ( href , ( e . state && e . state . scrollPosition ) || 0 )
255- await ( router . onAfterRouteChange ?? router . onAfterRouteChanged ) ?.( href )
231+ syncRouteQueryAndHash ( )
232+ await router . onAfterRouteChange ?.( href )
256233 } )
257234
258235 window . addEventListener ( 'hashchange' , ( e ) => {
259236 e . preventDefault ( )
237+ syncRouteQueryAndHash ( )
260238 } )
261239 }
262240
@@ -267,23 +245,24 @@ export function createRouter(
267245
268246export function useRouter ( ) : Router {
269247 const router = inject ( RouterSymbol )
270- if ( ! router ) {
271- throw new Error ( 'useRouter() is called without provider.' )
272- }
248+ if ( ! router ) throw new Error ( 'useRouter() is called without provider.' )
273249 return router
274250}
275251
276252export function useRoute ( ) : Route {
277253 return useRouter ( ) . route
278254}
279255
280- export function scrollTo ( el : Element , hash : string , smooth = false ) {
256+ export function scrollTo ( hash : string , smooth = false , scrollPosition = 0 ) {
257+ if ( ! hash || scrollPosition ) {
258+ window . scrollTo ( 0 , scrollPosition )
259+ return
260+ }
261+
281262 let target : Element | null = null
282263
283264 try {
284- target = el . classList . contains ( 'header-anchor' )
285- ? el
286- : document . getElementById ( decodeURIComponent ( hash ) . slice ( 1 ) )
265+ target = document . getElementById ( decodeURIComponent ( hash ) . slice ( 1 ) )
287266 } catch ( e ) {
288267 console . warn ( e )
289268 }
@@ -293,17 +272,20 @@ export function scrollTo(el: Element, hash: string, smooth = false) {
293272 window . getComputedStyle ( target ) . paddingTop ,
294273 10
295274 )
275+
296276 const targetTop =
297277 window . scrollY +
298278 target . getBoundingClientRect ( ) . top -
299279 getScrollOffset ( ) +
300280 targetPadding
281+
301282 function scrollToTarget ( ) {
302283 // only smooth scroll if distance is smaller than screen height.
303284 if ( ! smooth || Math . abs ( targetTop - window . scrollY ) > window . innerHeight )
304285 window . scrollTo ( 0 , targetTop )
305286 else window . scrollTo ( { left : 0 , top : targetTop , behavior : 'smooth' } )
306287 }
288+
307289 requestAnimationFrame ( scrollToTarget )
308290 }
309291}
@@ -313,9 +295,7 @@ function handleHMR(route: Route): void {
313295 if ( import . meta. hot ) {
314296 // hot reload pageData
315297 import . meta. hot . on ( 'vitepress:pageData' , ( payload : PageDataPayload ) => {
316- if ( shouldHotReload ( payload ) ) {
317- route . data = payload . pageData
318- }
298+ if ( shouldHotReload ( payload ) ) route . data = payload . pageData
319299 } )
320300 }
321301}
@@ -332,9 +312,47 @@ function normalizeHref(href: string): string {
332312 const url = new URL ( href , fakeHost )
333313 url . pathname = url . pathname . replace ( / ( ^ | \/ ) i n d e x ( \. h t m l ) ? $ / , '$1' )
334314 // ensure correct deep link so page refresh lands on correct files.
335- if ( siteDataRef . value . cleanUrls )
315+ if ( siteDataRef . value . cleanUrls ) {
336316 url . pathname = url . pathname . replace ( / \. h t m l $ / , '' )
337- else if ( ! url . pathname . endsWith ( '/' ) && ! url . pathname . endsWith ( '.html' ) )
317+ } else if ( ! url . pathname . endsWith ( '/' ) && ! url . pathname . endsWith ( '.html' ) ) {
338318 url . pathname += '.html'
319+ }
339320 return url . pathname + url . search + url . hash
340321}
322+
323+ async function changeRoute (
324+ href : string ,
325+ { smoothScroll = false , initialLoad = false } = { }
326+ ) : Promise < boolean > {
327+ const loc = normalizeHref ( location . href )
328+ const { pathname, hash } = new URL ( href , fakeHost )
329+ const currentLoc = new URL ( loc , fakeHost )
330+
331+ if ( href === loc ) {
332+ if ( ! initialLoad ) {
333+ scrollTo ( hash , smoothScroll )
334+ return false
335+ }
336+ } else {
337+ // save scroll position before changing URL
338+ history . replaceState ( { scrollPosition : window . scrollY } , '' )
339+ history . pushState ( { } , '' , href )
340+
341+ if ( pathname === currentLoc . pathname ) {
342+ // scroll between hash anchors on the same page, avoid duplicate entries
343+ if ( hash !== currentLoc . hash ) {
344+ window . dispatchEvent (
345+ new HashChangeEvent ( 'hashchange' , {
346+ oldURL : currentLoc . href ,
347+ newURL : href
348+ } )
349+ )
350+ scrollTo ( hash , smoothScroll )
351+ }
352+
353+ return false
354+ }
355+ }
356+
357+ return true
358+ }
0 commit comments