-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(qwik-city): improve scroll restoration, and 'popstate' and 'hash…
…change' navigations in SPAs - Support MPA-like and customizable scroll restoration in SPA - Race condition when restoring scroll positions - Prevent full-page reload when popstate happens before manual navigation - Fix hash navigation in SPA
- Loading branch information
Showing
20 changed files
with
704 additions
and
325 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,106 +1,45 @@ | ||
import type { QPrefetchData } from './service-worker/types'; | ||
import type { SimpleURL } from './types'; | ||
import { isSameOriginDifferentPathname, isSamePath, toPath, toUrl } from './utils'; | ||
import type { Signal } from '@builder.io/qwik'; | ||
import type { NavigationType } from './types'; | ||
import { isSameOrigin, isSamePath, toPath } from './utils'; | ||
|
||
export const clientNavigate = ( | ||
win: ClientHistoryWindow, | ||
newUrl: URL, | ||
routeNavigate: Signal<string> | ||
) => { | ||
const currentUrl = win.location; | ||
if (isSameOriginDifferentPathname(currentUrl, newUrl)) { | ||
// current browser url and route path are different | ||
// see if we should scroll to the hash after the url update | ||
handleScroll(win, currentUrl, newUrl); | ||
export const dispatchPrefetchEvent = (...links: string[]) => | ||
document.dispatchEvent(new CustomEvent('qprefetch', { detail: { links } })); | ||
|
||
// push the new route path to the history | ||
win.history.pushState('', '', toPath(newUrl)); | ||
} | ||
|
||
if (!win._qCityHistory) { | ||
// only add event listener once | ||
win._qCityHistory = 1; | ||
|
||
win.addEventListener('popstate', () => { | ||
// history pop event has happened | ||
const currentUrl = win.location; | ||
const previousUrl = toUrl(routeNavigate.value, currentUrl)!; | ||
|
||
if (isSameOriginDifferentPathname(currentUrl, previousUrl)) { | ||
handleScroll(win, previousUrl, currentUrl); | ||
// current browser url and route path are different | ||
// update the route path | ||
routeNavigate.value = toPath(new URL(currentUrl.href)); | ||
} | ||
}); | ||
|
||
win.removeEventListener('popstate', win._qCityPopstateFallback!); | ||
} | ||
}; | ||
|
||
const handleScroll = async (win: Window, previousUrl: SimpleURL, newUrl: SimpleURL) => { | ||
const doc = win.document; | ||
const newHash = newUrl.hash; | ||
|
||
if (isSamePath(previousUrl, newUrl)) { | ||
// same route after path change | ||
|
||
if (previousUrl.hash !== newHash) { | ||
// hash has changed on the same route | ||
|
||
// wait for a moment while window gets settled | ||
await domWait(); | ||
|
||
if (newHash) { | ||
// hash has changed on the same route and there's a hash | ||
// scroll to the element if it exists | ||
scrollToHashId(doc, newHash); | ||
} else { | ||
// hash has changed on the same route, but now there's no hash | ||
win.scrollTo(0, 0); | ||
export const clientNavigate = (win: Window, navType: NavigationType, fromUrl: URL, toUrl: URL) => { | ||
if (isSameOrigin(fromUrl, toUrl)) { | ||
if (navType === 'popstate') { | ||
clientHistoryState.id = win.history.state?.id ?? 0; | ||
} else { | ||
const samePath = isSamePath(fromUrl, toUrl); | ||
const sameHash = fromUrl.hash === toUrl.hash; | ||
// push to history for path or hash changes | ||
if (!samePath || !sameHash) { | ||
win.history.pushState({ id: ++clientHistoryState.id }, '', toPath(toUrl)); | ||
} | ||
} | ||
} else { | ||
// different route after change | ||
|
||
if (newHash) { | ||
// different route and there's a hash | ||
// content may not have finished updating yet | ||
// poll the dom querying for the element for a short time | ||
for (let i = 0; i < 24; i++) { | ||
await domWait(); | ||
if (scrollToHashId(doc, newHash)) { | ||
break; | ||
} | ||
// mimic native hashchange event | ||
if (navType === 'link' && samePath && !sameHash) { | ||
win.dispatchEvent( | ||
new win.HashChangeEvent('hashchange', { newURL: toUrl.href, oldURL: fromUrl.href }) | ||
); | ||
} | ||
} else { | ||
// different route and there isn't a hash | ||
await domWait(); | ||
win.scrollTo(0, 0); | ||
} | ||
} | ||
}; | ||
|
||
const domWait = () => new Promise((resolve) => setTimeout(resolve, 12)); | ||
/** | ||
* @alpha | ||
* @returns A unique opaque id representing the current client history entry | ||
*/ | ||
export const getHistoryId = () => '' + clientHistoryState.id; | ||
|
||
const scrollToHashId = (doc: Document, hash: string) => { | ||
const elmId = hash.slice(1); | ||
const elm = doc.getElementById(elmId); | ||
if (elm) { | ||
// found element to scroll to | ||
elm.scrollIntoView(); | ||
} | ||
return elm; | ||
}; | ||
/** | ||
* @internal | ||
*/ | ||
export const resetHistoryId = () => (clientHistoryState.id = 0); | ||
|
||
export const dispatchPrefetchEvent = (prefetchData: QPrefetchData) => { | ||
if (typeof document !== 'undefined') { | ||
document.dispatchEvent(new CustomEvent('qprefetch', { detail: prefetchData })); | ||
} | ||
}; | ||
const clientHistoryState = { id: 0 }; | ||
|
||
export interface ClientHistoryWindow extends Window { | ||
_qCityHistory?: 1; | ||
_qCityPopstateFallback?: () => void; | ||
declare global { | ||
interface Window { | ||
HashChangeEvent: new (type: string, eventInitDict?: HashChangeEventInit) => HashChangeEvent; | ||
} | ||
} |
Oops, something went wrong.