Files
sori.studio/plugins/site-route-scroll.client.js

125 lines
3.3 KiB
JavaScript

/**
* 공개 라우트 이동 후 새 화면 기준으로 스크롤 위치를 정리한다.
*
* Vue Router 기본 scrollBehavior는 전환 중 이전 화면을 먼저 움직일 수 있어
* `app/router.options.js`에서 끄고, 여기서는 Nuxt 페이지 렌더링이 끝난 뒤
* 새 화면에서만 최상단 또는 해시 위치로 이동한다. 전환 중에는 짧은 베일을
* 씌워 이전 화면이 상단으로 튀는 순간이 사용자에게 보이지 않게 한다.
*/
const ROUTE_VEIL_CLASS = 'site-route-veil'
const ROUTE_VEIL_TIMEOUT_MS = 8000
export default defineNuxtPlugin((nuxtApp) => {
const router = nuxtApp.$router
const root = document.documentElement
/** @type {{ id: number, to: import('vue-router').RouteLocationNormalizedLoaded, from: import('vue-router').RouteLocationNormalizedLoaded } | null} */
let pendingRouteScroll = null
let routeScrollId = 0
/** @type {ReturnType<typeof setTimeout> | null} */
let veilTimer = null
/**
* 브라우저 렌더 프레임을 기다린다.
* @returns {Promise<void>} 프레임 대기 Promise
*/
const waitForFrame = () => new Promise((resolve) => {
requestAnimationFrame(() => {
requestAnimationFrame(resolve)
})
})
/**
* 페이지 전환 베일을 표시한다.
* @returns {void}
*/
const showRouteVeil = () => {
root.classList.add(ROUTE_VEIL_CLASS)
clearTimeout(veilTimer)
veilTimer = setTimeout(() => {
root.classList.remove(ROUTE_VEIL_CLASS)
}, ROUTE_VEIL_TIMEOUT_MS)
}
/**
* 페이지 전환 베일을 숨긴다.
* @returns {void}
*/
const hideRouteVeil = () => {
clearTimeout(veilTimer)
veilTimer = null
root.classList.remove(ROUTE_VEIL_CLASS)
}
/**
* 이동 완료 후 적용할 스크롤 정보를 저장한다.
* @param {import('vue-router').RouteLocationNormalizedLoaded} to - 이동 대상 라우트
* @param {import('vue-router').RouteLocationNormalizedLoaded} from - 이전 라우트
* @returns {void}
*/
const queueRouteScroll = (to, from) => {
if (to.fullPath === from.fullPath) {
return
}
routeScrollId += 1
pendingRouteScroll = {
id: routeScrollId,
to,
from
}
}
/**
* 새 페이지 렌더링 이후 스크롤 위치를 적용한다.
* @returns {Promise<void>}
*/
const applyPendingRouteScroll = async () => {
const routeScroll = pendingRouteScroll
if (!routeScroll) {
hideRouteVeil()
return
}
await waitForFrame()
if (!pendingRouteScroll || pendingRouteScroll.id !== routeScroll.id) {
return
}
pendingRouteScroll = null
const { to } = routeScroll
if (to.hash) {
const targetId = decodeURIComponent(to.hash.slice(1))
const target = document.getElementById(targetId)
if (target) {
target.scrollIntoView({ block: 'start' })
await waitForFrame()
hideRouteVeil()
return
}
}
window.scrollTo({
top: 0,
left: 0,
behavior: 'auto'
})
await waitForFrame()
hideRouteVeil()
}
router.afterEach((to, from) => {
queueRouteScroll(to, from)
})
nuxtApp.hook('page:loading:start', showRouteVeil)
nuxtApp.hook('page:finish', applyPendingRouteScroll)
nuxtApp.hook('app:error', hideRouteVeil)
})