diff options
Diffstat (limited to 'examples/view-transitions/src/scripts/spa-navigation.js')
-rw-r--r-- | examples/view-transitions/src/scripts/spa-navigation.js | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/examples/view-transitions/src/scripts/spa-navigation.js b/examples/view-transitions/src/scripts/spa-navigation.js new file mode 100644 index 000000000..39159b5ef --- /dev/null +++ b/examples/view-transitions/src/scripts/spa-navigation.js @@ -0,0 +1,262 @@ +import { + getNavigationType, + getPathId, + isBackNavigation, + shouldNotIntercept, + updateTheDOMSomehow, + useTvFragment, +} from './utils' + +// View Transitions support cross-document navigations. +// Should compare performace. +// https://github.com/WICG/view-transitions/blob/main/explainer.md#cross-document-same-origin-transitions +// https://github.com/WICG/view-transitions/blob/main/explainer.md#script-events +function shouldDisableSpa() { + return false; +} + +navigation.addEventListener('navigate', (navigateEvent) => { + if (shouldDisableSpa()) return + if (shouldNotIntercept(navigateEvent)) return + + const toUrl = new URL(navigateEvent.destination.url) + const toPath = toUrl.pathname + const fromPath = location.pathname + const navigationType = getNavigationType(fromPath, toPath) + + if (location.origin !== toUrl.origin) return + + switch (navigationType) { + case 'home-to-movie': + case 'tv-to-show': + handleHomeToMovieTransition(navigateEvent, getPathId(toPath)) + break + case 'movie-to-home': + case 'show-to-tv': + handleMovieToHomeTransition(navigateEvent, getPathId(fromPath)) + break + case 'movie-to-person': + handleMovieToPersonTransition( + navigateEvent, + getPathId(fromPath), + getPathId(toPath) + ) + break + case 'person-to-movie': + case 'person-to-show': + handlePersonToMovieTransition( + navigateEvent, + getPathId(fromPath), + getPathId(toPath) + ) + break + default: + return + } +}) + +// TODO: https://developer.chrome.com/docs/web-platform/view-transitions/#transitions-as-an-enhancement +function handleHomeToMovieTransition(navigateEvent, movieId) { + navigateEvent.intercept({ + async handler() { + const fragmentUrl = useTvFragment(navigateEvent) + ? '/fragments/TvDetails' + : '/fragments/MovieDetails' + const response = await fetch(`${fragmentUrl}/${movieId}`) + const data = await response.text() + + if (!document.startViewTransition) { + updateTheDOMSomehow(data); + return; + } + + const thumbnail = document.getElementById(`movie-poster-${movieId}`) + if (thumbnail) { + thumbnail.style.viewTransitionName = 'movie-poster' + } + + const transition = document.startViewTransition(() => { + if (thumbnail) { + thumbnail.style.viewTransitionName = '' + } + document.getElementById('container').scrollTop = 0 + updateTheDOMSomehow(data) + }) + + await transition.finished + }, + }) +} + +function handleMovieToHomeTransition(navigateEvent, movieId) { + navigateEvent.intercept({ + scroll: 'manual', + async handler() { + const fragmentUrl = useTvFragment(navigateEvent) + ? '/fragments/TvList' + : '/fragments/MovieList' + const response = await fetch(fragmentUrl) + const data = await response.text() + + if (!document.startViewTransition) { + updateTheDOMSomehow(data) + return + } + + const tempHomePage = document.createElement('div') + const moviePoster = document.getElementById(`movie-poster`) + let thumbnail + + // If the movie poster is not in the home page, removes the transition style so that + // the poster doesn't stay on the page while transitioning + tempHomePage.innerHTML = data + if (!tempHomePage.querySelector(`#movie-poster-${movieId}`)) { + moviePoster?.classList.remove('movie-poster') + } + + const transition = document.startViewTransition(() => { + updateTheDOMSomehow(data) + + thumbnail = document.getElementById(`movie-poster-${movieId}`) + if (thumbnail) { + thumbnail.scrollIntoViewIfNeeded() + thumbnail.style.viewTransitionName = 'movie-poster' + } + }) + + await transition.finished + + if (thumbnail) { + thumbnail.style.viewTransitionName = '' + } + }, + }) +} + +function handleMovieToPersonTransition(navigateEvent, movieId, personId) { + // TODO: https://developer.chrome.com/docs/web-platform/view-transitions/#not-a-polyfill + // ...has example of `back-transition` class applied to document + const isBack = isBackNavigation(navigateEvent) + + navigateEvent.intercept({ + async handler() { + const response = await fetch('/fragments/PersonDetails/' + personId) + const data = await response.text() + + if (!document.startViewTransition) { + updateTheDOMSomehow(data) + return + } + + let personThumbnail + let moviePoster + let movieThumbnail + + if (!isBack) { + // We're transitioning the person photo; we need to remove the transition of the poster + // so that it doesn't stay on the page while transitioning + moviePoster = document.getElementById(`movie-poster`) + if (moviePoster) { + moviePoster.classList.remove('movie-poster') + } + + personThumbnail = document.getElementById(`person-photo-${personId}`) + if (personThumbnail) { + personThumbnail.style.viewTransitionName = 'person-photo' + } + } + + const transition = document.startViewTransition(() => { + updateTheDOMSomehow(data) + + if (personThumbnail) { + personThumbnail.style.viewTransitionName = '' + } + + if (isBack) { + // If we're coming back to the person page, we're transitioning + // into the movie poster thumbnail, so we need to add the tag to it + movieThumbnail = document.getElementById(`movie-poster-${movieId}`) + if (movieThumbnail) { + movieThumbnail.scrollIntoViewIfNeeded() + movieThumbnail.style.viewTransitionName = 'movie-poster' + } + } + + document.getElementById('container').scrollTop = 0 + }) + + await transition.finished + + if (movieThumbnail) { + movieThumbnail.style.viewTransitionName = '' + } + }, + }) +} + +function handlePersonToMovieTransition(navigateEvent, personId, movieId) { + const isBack = isBackNavigation(navigateEvent) + + navigateEvent.intercept({ + scroll: 'manual', + async handler() { + const fragmentUrl = useTvFragment(navigateEvent) + ? '/fragments/TvDetails' + : '/fragments/MovieDetails' + const response = await fetch(`${fragmentUrl}/${movieId}`) + const data = await response.text() + + if (!document.startViewTransition) { + updateTheDOMSomehow(data) + return + } + + let thumbnail + let moviePoster + let movieThumbnail + + if (!isBack) { + movieThumbnail = document.getElementById(`movie-poster-${movieId}`) + if (movieThumbnail) { + movieThumbnail.style.viewTransitionName = 'movie-poster' + } + } + + const transition = document.startViewTransition(() => { + updateTheDOMSomehow(data) + + if (isBack) { + moviePoster = document.getElementById(`movie-poster`) + if (moviePoster) { + moviePoster.classList.remove('movie-poster') + } + + if (personId) { + thumbnail = document.getElementById(`person-photo-${personId}`) + if (thumbnail) { + thumbnail.scrollIntoViewIfNeeded() + thumbnail.style.viewTransitionName = 'person-photo' + } + } + } else { + document.getElementById('container').scrollTop = 0 + + if (movieThumbnail) { + movieThumbnail.style.viewTransitionName = '' + } + } + }) + + await transition.finished + + if (thumbnail) { + thumbnail.style.viewTransitionName = '' + } + + if (moviePoster) { + moviePoster.classList.add('movie-poster') + } + }, + }) +} |