1+ import { onMount } from 'svelte' ;
12import { get_base_uri } from './utils' ;
23
34function scroll_state ( ) {
@@ -53,8 +54,21 @@ export class Router {
5354 // make it possible to reset focus
5455 document . body . setAttribute ( 'tabindex' , '-1' ) ;
5556
56- // create initial history entry, so we can return here
57- history . replaceState ( history . state || { } , '' , location . href ) ;
57+ // keeping track of the history index in order to prevent popstate navigation events if needed
58+ this . current_history_index = history . state ?. [ 'sveltekit:index' ] ?? 0 ;
59+
60+ if ( this . current_history_index === 0 ) {
61+ // create initial history entry, so we can return here
62+ history . replaceState ( { ...history . state , 'sveltekit:index' : 0 } , '' , location . href ) ;
63+ }
64+
65+ this . callbacks = {
66+ /** @type {Array<({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void> } */
67+ before_navigate : [ ] ,
68+
69+ /** @type {Array<({ from, to }: { from: URL | null, to: URL }) => void> } */
70+ after_navigate : [ ]
71+ } ;
5872 }
5973
6074 init_listeners ( ) {
@@ -66,8 +80,23 @@ export class Router {
6680 // Reset scrollRestoration to auto when leaving page, allowing page reload
6781 // and back-navigation from other pages to use the browser to restore the
6882 // scrolling position.
69- addEventListener ( 'beforeunload' , ( ) => {
70- history . scrollRestoration = 'auto' ;
83+ addEventListener ( 'beforeunload' , ( e ) => {
84+ let should_block = false ;
85+
86+ const intent = {
87+ from : this . renderer . current . url ,
88+ to : null ,
89+ cancel : ( ) => ( should_block = true )
90+ } ;
91+
92+ this . callbacks . before_navigate . forEach ( ( fn ) => fn ( intent ) ) ;
93+
94+ if ( should_block ) {
95+ e . preventDefault ( ) ;
96+ e . returnValue = '' ;
97+ } else {
98+ history . scrollRestoration = 'auto' ;
99+ }
71100 } ) ;
72101
73102 // Setting scrollRestoration to manual again when returning to this page.
@@ -122,7 +151,7 @@ export class Router {
122151 addEventListener ( 'sveltekit:trigger_prefetch' , trigger_prefetch ) ;
123152
124153 /** @param {MouseEvent } event */
125- addEventListener ( 'click' , ( event ) => {
154+ addEventListener ( 'click' , async ( event ) => {
126155 if ( ! this . enabled ) return ;
127156
128157 // Adapted from https://github.com/visionmedia/page.js
@@ -155,8 +184,6 @@ export class Router {
155184 // Ignore if <a> has a target
156185 if ( a instanceof SVGAElement ? a . target . baseVal : a . target ) return ;
157186
158- if ( ! this . owns ( url ) ) return ;
159-
160187 // Check if new url only differs by hash
161188 if ( url . href . split ( '#' ) [ 0 ] === location . href . split ( '#' ) [ 0 ] ) {
162189 // Call `pushState` to add url to history so going back works.
@@ -169,20 +196,48 @@ export class Router {
169196 return ;
170197 }
171198
172- const noscroll = a . hasAttribute ( 'sveltekit:noscroll' ) ;
173- this . _navigate ( url , noscroll ? scroll_state ( ) : null , false , [ ] , url . hash , { } , 'pushState' ) ;
174- event . preventDefault ( ) ;
199+ this . _navigate ( {
200+ url,
201+ scroll : a . hasAttribute ( 'sveltekit:noscroll' ) ? scroll_state ( ) : null ,
202+ keepfocus : false ,
203+ chain : [ ] ,
204+ details : {
205+ state : { } ,
206+ replaceState : false
207+ } ,
208+ accepted : ( ) => event . preventDefault ( ) ,
209+ blocked : ( ) => event . preventDefault ( )
210+ } ) ;
175211 } ) ;
176212
177213 addEventListener ( 'popstate' , ( event ) => {
178214 if ( event . state && this . enabled ) {
179- const url = new URL ( location . href ) ;
180- this . _navigate ( url , event . state [ 'sveltekit:scroll' ] , false , [ ] , url . hash , null , null ) ;
215+ // if a popstate-driven navigation is cancelled, we need to counteract it
216+ // with history.go, which means we end up back here, hence this check
217+ if ( event . state [ 'sveltekit:index' ] === this . current_history_index ) return ;
218+
219+ this . _navigate ( {
220+ url : new URL ( location . href ) ,
221+ scroll : event . state [ 'sveltekit:scroll' ] ,
222+ keepfocus : false ,
223+ chain : [ ] ,
224+ details : null ,
225+ accepted : ( ) => {
226+ this . current_history_index = event . state [ 'sveltekit:index' ] ;
227+ } ,
228+ blocked : ( ) => {
229+ const delta = this . current_history_index - event . state [ 'sveltekit:index' ] ;
230+ history . go ( delta ) ;
231+ }
232+ } ) ;
181233 }
182234 } ) ;
183235 }
184236
185- /** @param {URL } url */
237+ /**
238+ * Returns true if `url` has the same origin and basepath as the app
239+ * @param {URL } url
240+ */
186241 owns ( url ) {
187242 return url . origin === location . origin && url . pathname . startsWith ( this . base ) ;
188243 }
@@ -218,16 +273,19 @@ export class Router {
218273 ) {
219274 const url = new URL ( href , get_base_uri ( document ) ) ;
220275
221- if ( this . enabled && this . owns ( url ) ) {
222- return this . _navigate (
276+ if ( this . enabled ) {
277+ return this . _navigate ( {
223278 url,
224- noscroll ? scroll_state ( ) : null ,
279+ scroll : noscroll ? scroll_state ( ) : null ,
225280 keepfocus,
226281 chain,
227- url . hash ,
228- state ,
229- replaceState ? 'replaceState' : 'pushState'
230- ) ;
282+ details : {
283+ state,
284+ replaceState
285+ } ,
286+ accepted : ( ) => { } ,
287+ blocked : ( ) => { }
288+ } ) ;
231289 }
232290
233291 location . href = url . href ;
@@ -258,22 +316,73 @@ export class Router {
258316 return this . renderer . load ( info ) ;
259317 }
260318
319+ /** @param {({ from, to }: { from: URL | null, to: URL }) => void } fn */
320+ after_navigate ( fn ) {
321+ onMount ( ( ) => {
322+ this . callbacks . after_navigate . push ( fn ) ;
323+
324+ return ( ) => {
325+ const i = this . callbacks . after_navigate . indexOf ( fn ) ;
326+ this . callbacks . after_navigate . splice ( i , 1 ) ;
327+ } ;
328+ } ) ;
329+ }
330+
261331 /**
262- * @param {URL } url
263- * @param {{ x: number, y: number }? } scroll
264- * @param {boolean } keepfocus
265- * @param {string[] } chain
266- * @param {string } hash
267- * @param {any } state
268- * @param {'pushState' | 'replaceState' | null } method
332+ * @param {({ from, to, cancel }: { from: URL, to: URL | null, cancel: () => void }) => void } fn
269333 */
270- async _navigate ( url , scroll , keepfocus , chain , hash , state , method ) {
271- const info = this . parse ( url ) ;
334+ before_navigate ( fn ) {
335+ onMount ( ( ) => {
336+ this . callbacks . before_navigate . push ( fn ) ;
337+
338+ return ( ) => {
339+ const i = this . callbacks . before_navigate . indexOf ( fn ) ;
340+ this . callbacks . before_navigate . splice ( i , 1 ) ;
341+ } ;
342+ } ) ;
343+ }
272344
345+ /**
346+ * @param {{
347+ * url: URL;
348+ * scroll: { x: number, y: number } | null;
349+ * keepfocus: boolean;
350+ * chain: string[];
351+ * details: {
352+ * replaceState: boolean;
353+ * state: any;
354+ * } | null;
355+ * accepted: () => void;
356+ * blocked: () => void;
357+ * }} opts
358+ */
359+ async _navigate ( { url, scroll, keepfocus, chain, details, accepted, blocked } ) {
360+ const from = this . renderer . current . url ;
361+ let should_block = false ;
362+
363+ const intent = {
364+ from,
365+ to : url ,
366+ cancel : ( ) => ( should_block = true )
367+ } ;
368+
369+ this . callbacks . before_navigate . forEach ( ( fn ) => fn ( intent ) ) ;
370+
371+ if ( should_block ) {
372+ blocked ( ) ;
373+ return ;
374+ }
375+
376+ const info = this . parse ( url ) ;
273377 if ( ! info ) {
274- throw new Error ( 'Attempted to navigate to a URL that does not belong to this app' ) ;
378+ location . href = url . href ;
379+ return new Promise ( ( ) => {
380+ // never resolves
381+ } ) ;
275382 }
276383
384+ accepted ( ) ;
385+
277386 if ( ! this . navigating ) {
278387 dispatchEvent ( new CustomEvent ( 'sveltekit:navigation-start' ) ) ;
279388 }
@@ -289,13 +398,24 @@ export class Router {
289398 }
290399
291400 info . url = new URL ( url . origin + pathname + url . search + url . hash ) ;
292- if ( method ) history [ method ] ( state , '' , info . url ) ;
293401
294- await this . renderer . handle_navigation ( info , chain , false , { hash, scroll, keepfocus } ) ;
402+ if ( details ) {
403+ const change = details . replaceState ? 0 : 1 ;
404+ details . state [ 'sveltekit:index' ] = this . current_history_index += change ;
405+ history [ details . replaceState ? 'replaceState' : 'pushState' ] ( details . state , '' , info . url ) ;
406+ }
407+
408+ await this . renderer . handle_navigation ( info , chain , false , {
409+ scroll,
410+ keepfocus
411+ } ) ;
295412
296413 this . navigating -- ;
297414 if ( ! this . navigating ) {
298415 dispatchEvent ( new CustomEvent ( 'sveltekit:navigation-end' ) ) ;
416+
417+ const navigation = { from, to : url } ;
418+ this . callbacks . after_navigate . forEach ( ( fn ) => fn ( navigation ) ) ;
299419 }
300420 }
301421}
0 commit comments