@@ -16,197 +16,183 @@ import { event$, isDev } from '@qwik.dev/core';
16
16
// ! DO NOT IMPORT OR USE ANY EXTERNAL REFERENCES IN THIS SCRIPT.
17
17
export default event$ ( ( _ : Event , el : Element ) => {
18
18
const win : ClientSPAWindow = window ;
19
- const spa = '_qRouterSPA' ;
20
- const initPopstate = '_qRouterInitPopstate' ;
21
- const initAnchors = '_qRouterInitAnchors' ;
22
- const initVisibility = '_qRouterInitVisibility' ;
23
- const initScroll = '_qRouterInitScroll' ;
24
- if (
25
- ! win [ spa ] &&
26
- ! win [ initPopstate ] &&
27
- ! win [ initAnchors ] &&
28
- ! win [ initVisibility ] &&
29
- ! win [ initScroll ]
30
- ) {
31
- const currentPath = location . pathname + location . search ;
32
-
33
- const historyPatch = '_qRouterHistoryPatch' ;
34
- const scrollEnabled = '_qRouterScrollEnabled' ;
35
- const debounceTimeout = '_qRouterScrollDebounce' ;
36
- const scrollHistory = '_qRouterScroll' ;
37
-
38
- const checkAndScroll = ( scrollState : ScrollState | undefined ) => {
39
- if ( scrollState ) {
40
- win . scrollTo ( scrollState . x , scrollState . y ) ;
41
- }
42
- } ;
19
+ if ( win . _qRouterInitPopstate ) {
20
+ return ;
21
+ }
22
+ const currentPath = location . pathname + location . search ;
43
23
44
- const currentScrollState = ( ) : ScrollState => {
45
- const elm = document . documentElement ;
46
- return {
47
- x : elm . scrollLeft ,
48
- y : elm . scrollTop ,
49
- w : Math . max ( elm . scrollWidth , elm . clientWidth ) ,
50
- h : Math . max ( elm . scrollHeight , elm . clientHeight ) ,
51
- } ;
24
+ const checkAndScroll = ( scrollState : ScrollState | undefined ) => {
25
+ if ( scrollState ) {
26
+ win . scrollTo ( scrollState . x , scrollState . y ) ;
27
+ }
28
+ } ;
29
+
30
+ const currentScrollState = ( ) : ScrollState => {
31
+ const elm = document . documentElement ;
32
+ return {
33
+ x : elm . scrollLeft ,
34
+ y : elm . scrollTop ,
35
+ w : Math . max ( elm . scrollWidth , elm . clientWidth ) ,
36
+ h : Math . max ( elm . scrollHeight , elm . clientHeight ) ,
52
37
} ;
38
+ } ;
53
39
54
- const saveScrollState = ( scrollState ?: ScrollState ) => {
55
- const state : ScrollHistoryState = history . state || { } ;
56
- state [ scrollHistory ] = scrollState || currentScrollState ( ) ;
57
- history . replaceState ( state , '' ) ;
58
- } ;
40
+ const saveScrollState = ( scrollState ?: ScrollState ) => {
41
+ const state : ScrollHistoryState = history . state || { } ;
42
+ state . _qRouterScroll = scrollState || currentScrollState ( ) ;
43
+ history . replaceState ( state , '' ) ;
44
+ } ;
59
45
60
- saveScrollState ( ) ;
46
+ saveScrollState ( ) ;
61
47
62
- win [ initPopstate ] = ( ) => {
63
- if ( win [ spa ] ) {
64
- return ;
65
- }
48
+ // Also used in qwik-router-component.tsx to unregister
49
+ win . _qRouterInitPopstate = ( ) => {
50
+ if ( win . _qRouterSPA ) {
51
+ return ;
52
+ }
66
53
67
- // Disable scroll handler eagerly to prevent overwriting history.state.
68
- win [ scrollEnabled ] = false ;
69
- clearTimeout ( win [ debounceTimeout ] ) ;
54
+ // Disable scroll handler eagerly to prevent overwriting history.state.
55
+ win . _qRouterScrollEnabled = false ;
56
+ clearTimeout ( win . _qRouterScrollDebounce ) ;
70
57
71
- if ( currentPath !== location . pathname + location . search ) {
72
- const getContainer = ( el : Element ) =>
73
- el . closest ( '[q\\:container]:not([q\\:container=html]):not([q\\:container=text])' ) ;
58
+ if ( currentPath !== location . pathname + location . search ) {
59
+ const getContainer = ( el : Element ) =>
60
+ el . closest ( '[q\\:container]:not([q\\:container=html]):not([q\\:container=text])' ) ;
74
61
75
- const container = getContainer ( el ) ;
76
- const domContainer = ( container as _ContainerElement ) . qContainer as DomContainer ;
77
- const hostElement = domContainer . vNodeLocate ( el ) ;
62
+ const container = getContainer ( el ) ;
63
+ const domContainer = ( container as _ContainerElement ) . qContainer as DomContainer ;
64
+ const hostElement = domContainer . vNodeLocate ( el ) ;
78
65
79
- const nav = domContainer ?. resolveContext ( hostElement , {
80
- id : 'qc--n' ,
81
- } as ContextId < RouteNavigate > ) ;
66
+ const nav = domContainer ?. resolveContext ( hostElement , {
67
+ id : 'qc--n' ,
68
+ } as ContextId < RouteNavigate > ) ;
82
69
83
- if ( nav ) {
84
- nav ( location . href , { type : 'popstate' } ) ;
85
- } else {
86
- // No useNavigate ctx available, fallback to reload.
87
- location . reload ( ) ;
88
- }
70
+ if ( nav ) {
71
+ nav ( location . href , { type : 'popstate' } ) ;
89
72
} else {
90
- if ( history . scrollRestoration === 'manual' ) {
91
- const scrollState = ( history . state as ScrollHistoryState ) ?. [ scrollHistory ] ;
92
- checkAndScroll ( scrollState ) ;
93
- win [ scrollEnabled ] = true ;
73
+ // No useNavigate ctx available, fallback to reload.
74
+ location . reload ( ) ;
75
+ }
76
+ } else {
77
+ if ( history . scrollRestoration === 'manual' ) {
78
+ const scrollState = ( history . state as ScrollHistoryState ) ?. _qRouterScroll ;
79
+ checkAndScroll ( scrollState ) ;
80
+ win . _qRouterScrollEnabled = true ;
81
+ }
82
+ }
83
+ } ;
84
+
85
+ if ( ! win . _qRouterHistoryPatch ) {
86
+ win . _qRouterHistoryPatch = true ;
87
+ const pushState = history . pushState ;
88
+ const replaceState = history . replaceState ;
89
+
90
+ const prepareState = ( state : any ) => {
91
+ if ( state === null || typeof state === 'undefined' ) {
92
+ state = { } ;
93
+ } else if ( state ?. constructor !== Object ) {
94
+ state = { _data : state } ;
95
+
96
+ if ( isDev ) {
97
+ console . warn (
98
+ 'In a Qwik SPA context, `history.state` is used to store scroll state. ' +
99
+ 'Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. ' +
100
+ 'We need to be able to automatically attach the scroll state to your state object. ' +
101
+ 'A new state object has been created, your data has been moved to: `history.state._data`'
102
+ ) ;
94
103
}
95
104
}
96
- } ;
97
105
98
- if ( ! win [ historyPatch ] ) {
99
- win [ historyPatch ] = true ;
100
- const pushState = history . pushState ;
101
- const replaceState = history . replaceState ;
102
-
103
- const prepareState = ( state : any ) => {
104
- if ( state === null || typeof state === 'undefined' ) {
105
- state = { } ;
106
- } else if ( state ?. constructor !== Object ) {
107
- state = { _data : state } ;
108
-
109
- if ( isDev ) {
110
- console . warn (
111
- 'In a Qwik SPA context, `history.state` is used to store scroll state. ' +
112
- 'Direct calls to `pushState()` and `replaceState()` must supply an actual Object type. ' +
113
- 'We need to be able to automatically attach the scroll state to your state object. ' +
114
- 'A new state object has been created, your data has been moved to: `history.state._data`'
115
- ) ;
116
- }
117
- }
106
+ state . _qRouterScroll = state . _qRouterScroll || currentScrollState ( ) ;
107
+ return state ;
108
+ } ;
118
109
119
- state . _qRouterScroll = state . _qRouterScroll || currentScrollState ( ) ;
120
- return state ;
121
- } ;
110
+ history . pushState = ( state , title , url ) => {
111
+ state = prepareState ( state ) ;
112
+ return pushState . call ( history , state , title , url ) ;
113
+ } ;
122
114
123
- history . pushState = ( state , title , url ) => {
124
- state = prepareState ( state ) ;
125
- return pushState . call ( history , state , title , url ) ;
126
- } ;
115
+ history . replaceState = ( state , title , url ) => {
116
+ state = prepareState ( state ) ;
117
+ return replaceState . call ( history , state , title , url ) ;
118
+ } ;
119
+ }
127
120
128
- history . replaceState = ( state , title , url ) => {
129
- state = prepareState ( state ) ;
130
- return replaceState . call ( history , state , title , url ) ;
131
- } ;
121
+ // We need this handler in init because Firefox destroys states w/ anchor tags.
122
+ win . _qRouterInitAnchors = ( event : MouseEvent ) => {
123
+ if ( win . _qRouterSPA || event . defaultPrevented ) {
124
+ return ;
132
125
}
133
126
134
- // We need this handler in init because Firefox destroys states w/ anchor tags.
135
- win [ initAnchors ] = ( event : MouseEvent ) => {
136
- if ( win [ spa ] || event . defaultPrevented ) {
137
- return ;
138
- }
139
-
140
- const target = ( event . target as HTMLElement ) . closest ( 'a[href]' ) ;
141
-
142
- if ( target && ! target . hasAttribute ( 'preventdefault:click' ) ) {
143
- const href = target . getAttribute ( 'href' ) ! ;
144
- const prev = new URL ( location . href ) ;
145
- const dest = new URL ( href , prev ) ;
146
- const sameOrigin = dest . origin === prev . origin ;
147
- const samePath = dest . pathname + dest . search === prev . pathname + prev . search ;
148
- // Patch only same-page anchors.
149
- if ( sameOrigin && samePath ) {
150
- event . preventDefault ( ) ;
151
-
152
- // Check href because empty hashes don't register.
153
- if ( dest . href !== prev . href ) {
154
- history . pushState ( null , '' , dest ) ;
155
- }
127
+ const target = ( event . target as HTMLElement ) . closest ( 'a[href]' ) ;
128
+
129
+ if ( target && ! target . hasAttribute ( 'preventdefault:click' ) ) {
130
+ const href = target . getAttribute ( 'href' ) ! ;
131
+ const prev = new URL ( location . href ) ;
132
+ const dest = new URL ( href , prev ) ;
133
+ const sameOrigin = dest . origin === prev . origin ;
134
+ const samePath = dest . pathname + dest . search === prev . pathname + prev . search ;
135
+ // Patch only same-page anchors.
136
+ if ( sameOrigin && samePath ) {
137
+ event . preventDefault ( ) ;
138
+
139
+ // Check href because empty hashes don't register.
140
+ if ( dest . href !== prev . href ) {
141
+ history . pushState ( null , '' , dest ) ;
142
+ }
156
143
157
- if ( ! dest . hash ) {
158
- if ( dest . href . endsWith ( '#' ) ) {
159
- window . scrollTo ( 0 , 0 ) ;
160
- } else {
161
- // Simulate same-page (no hash) anchor reload.
162
- // history.scrollRestoration = 'manual' makes these not scroll.
163
- win [ scrollEnabled ] = false ;
164
- clearTimeout ( win [ debounceTimeout ] ) ;
165
- saveScrollState ( { ...currentScrollState ( ) , x : 0 , y : 0 } ) ;
166
- location . reload ( ) ;
167
- }
144
+ if ( ! dest . hash ) {
145
+ if ( dest . href . endsWith ( '#' ) ) {
146
+ window . scrollTo ( 0 , 0 ) ;
168
147
} else {
169
- const elmId = dest . hash . slice ( 1 ) ;
170
- const elm = document . getElementById ( elmId ) ;
171
- if ( elm ) {
172
- elm . scrollIntoView ( ) ;
173
- }
148
+ // Simulate same-page (no hash) anchor reload.
149
+ // history.scrollRestoration = 'manual' makes these not scroll.
150
+ win . _qRouterScrollEnabled = false ;
151
+ clearTimeout ( win . _qRouterScrollDebounce ) ;
152
+ saveScrollState ( { ...currentScrollState ( ) , x : 0 , y : 0 } ) ;
153
+ location . reload ( ) ;
154
+ }
155
+ } else {
156
+ const elmId = dest . hash . slice ( 1 ) ;
157
+ const elm = document . getElementById ( elmId ) ;
158
+ if ( elm ) {
159
+ elm . scrollIntoView ( ) ;
174
160
}
175
161
}
176
162
}
177
- } ;
178
-
179
- win [ initVisibility ] = ( ) => {
180
- if ( ! win [ spa ] && win [ scrollEnabled ] && document . visibilityState === 'hidden' ) {
181
- saveScrollState ( ) ;
182
- }
183
- } ;
184
-
185
- win [ initScroll ] = ( ) => {
186
- if ( win [ spa ] || ! win [ scrollEnabled ] ) {
187
- return ;
188
- }
189
-
190
- clearTimeout ( win [ debounceTimeout ] ) ;
191
- win [ debounceTimeout ] = setTimeout ( ( ) => {
192
- saveScrollState ( ) ;
193
- // Needed for e2e debounceDetector.
194
- win [ debounceTimeout ] = undefined ;
195
- } , 200 ) ;
196
- } ;
163
+ }
164
+ } ;
197
165
198
- win [ scrollEnabled ] = true ;
166
+ win . _qRouterInitVisibility = ( ) => {
167
+ if ( ! win . _qRouterSPA && win . _qRouterScrollEnabled && document . visibilityState === 'hidden' ) {
168
+ saveScrollState ( ) ;
169
+ }
170
+ } ;
199
171
200
- setTimeout ( ( ) => {
201
- win . addEventListener ( 'popstate' , win [ initPopstate ] ! ) ;
202
- win . addEventListener ( 'scroll' , win [ initScroll ] ! , { passive : true } ) ;
203
- document . body . addEventListener ( 'click' , win [ initAnchors ] ! ) ;
172
+ win . _qRouterInitScroll = ( ) => {
173
+ if ( win . _qRouterSPA || ! win . _qRouterScrollEnabled ) {
174
+ return ;
175
+ }
204
176
205
- if ( ! ( win as any ) . navigation ) {
206
- document . addEventListener ( 'visibilitychange' , win [ initVisibility ] ! , {
207
- passive : true ,
208
- } ) ;
209
- }
210
- } , 0 ) ;
211
- }
177
+ clearTimeout ( win . _qRouterScrollDebounce ) ;
178
+ win . _qRouterScrollDebounce = setTimeout ( ( ) => {
179
+ saveScrollState ( ) ;
180
+ // Needed for e2e debounceDetector.
181
+ win . _qRouterScrollDebounce = undefined ;
182
+ } , 200 ) ;
183
+ } ;
184
+
185
+ win . _qRouterScrollEnabled = true ;
186
+
187
+ setTimeout ( ( ) => {
188
+ win . addEventListener ( 'popstate' , win . _qRouterInitPopstate ! ) ;
189
+ win . addEventListener ( 'scroll' , win . _qRouterInitScroll ! , { passive : true } ) ;
190
+ document . body . addEventListener ( 'click' , win . _qRouterInitAnchors ! ) ;
191
+
192
+ if ( ! ( win as any ) . navigation ) {
193
+ document . addEventListener ( 'visibilitychange' , win . _qRouterInitVisibility ! , {
194
+ passive : true ,
195
+ } ) ;
196
+ }
197
+ } , 0 ) ;
212
198
} ) ;
0 commit comments