diff options
Diffstat (limited to 'examples/view-transitions/src/scripts')
-rw-r--r-- | examples/view-transitions/src/scripts/spa-navigation.js | 262 | ||||
-rw-r--r-- | examples/view-transitions/src/scripts/utils.js | 79 |
2 files changed, 341 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') + } + }, + }) +} diff --git a/examples/view-transitions/src/scripts/utils.js b/examples/view-transitions/src/scripts/utils.js new file mode 100644 index 000000000..3d98181ab --- /dev/null +++ b/examples/view-transitions/src/scripts/utils.js @@ -0,0 +1,79 @@ +export function getNavigationType(fromPath, toPath) { + if (fromPath.startsWith('/movies') && toPath === '/') { + return 'movie-to-home' + } + + if (fromPath === '/tv' && toPath.startsWith('/tv/')) { + return 'tv-to-show' + } + + if (fromPath === '/' && toPath.startsWith('/movies')) { + return 'home-to-movie' + } + + if (fromPath.startsWith('/tv/') && toPath === '/tv') { + return 'show-to-tv' + } + + if ( + (fromPath.startsWith('/movies') || fromPath.startsWith('/tv')) && + toPath.startsWith('/people') + ) { + return 'movie-to-person' + } + + if ( + fromPath.startsWith('/people') && + (toPath.startsWith('/movies') || toPath.startsWith('/tv/')) + ) { + return 'person-to-movie' + } + + return 'other' +} + +export function isBackNavigation(navigateEvent) { + if ( + navigateEvent.navigationType === 'push' || + navigateEvent.navigationType === 'replace' + ) { + return false + } + if ( + navigateEvent.destination.index !== -1 && + navigateEvent.destination.index < navigation.currentEntry.index + ) { + return true + } + return false +} + +export function shouldNotIntercept(navigationEvent) { + return ( + navigationEvent.canIntercept === false || + // If this is just a hashChange, + // just let the browser handle scrolling to the content. + navigationEvent.hashChange || + // If this is a download, + // let the browser perform the download. + navigationEvent.downloadRequest || + // If this is a form submission, + // let that go to the server. + navigationEvent.formData + ) +} + +export function useTvFragment(navigateEvent) { + const toUrl = new URL(navigateEvent.destination.url) + const toPath = toUrl.pathname + + return toPath.startsWith('/tv') +} + +export function getPathId(path) { + return path.split('/')[2] +} + +export function updateTheDOMSomehow(data) { + document.getElementById('content').innerHTML = data +} |