import { consumePendingScrollRestore, scrollDocumentTo, scrollDocumentToTop, shouldUseSiteRouteVeil } from '../lib/site-route-transition.js' const SITE_ROUTE_VEIL_CLASS = 'site-page-veil' /** * 공개 페이지 이동 시 전환 베일을 표시하고, 베일 뒤에서 스크롤을 정리한 뒤 새 화면을 드러낸다. */ export default defineNuxtPlugin((nuxtApp) => { const router = useRouter() const isSitePageVeiled = useState('site-page-veil', () => false) /** @type {boolean} */ let activeVeilNavigation = false /** * 전환 베일을 즉시 표시한다. * @returns {void} */ const showRouteVeil = () => { if (document.documentElement.classList.contains(SITE_ROUTE_VEIL_CLASS)) { return } activeVeilNavigation = true isSitePageVeiled.value = true document.documentElement.classList.add(SITE_ROUTE_VEIL_CLASS) } /** * 전환 베일을 즉시 숨긴다. * @returns {void} */ const hideRouteVeil = () => { activeVeilNavigation = false isSitePageVeiled.value = false document.documentElement.classList.remove(SITE_ROUTE_VEIL_CLASS) } /** * 내부 링크 클릭이 라우터보다 먼저 베일을 덮도록 한다. * @param {MouseEvent} event - 클릭 이벤트 * @returns {void} */ const handleInternalLinkCapture = (event) => { if (event.defaultPrevented || event.button !== 0) { return } if (event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) { return } const target = event.target if (!(target instanceof Element)) { return } const anchor = target.closest('a') if (!anchor || anchor.target === '_blank' || anchor.hasAttribute('download')) { return } const href = anchor.getAttribute('href') if (!href || href.startsWith('#') || href.startsWith('mailto:') || href.startsWith('tel:')) { return } let url = null try { url = new URL(anchor.href, window.location.origin) } catch { return } if (url.origin !== window.location.origin) { return } const currentPath = router.currentRoute.value.path if (!shouldUseSiteRouteVeil(url.pathname) && !shouldUseSiteRouteVeil(currentPath)) { return } if (url.pathname === currentPath && !url.hash) { return } showRouteVeil() } /** * 페이지 렌더 완료 후 스크롤을 맞추고 베일을 해제한다. * @returns {Promise} */ const finishRouteTransition = async () => { if (!activeVeilNavigation) { return } const route = router.currentRoute.value const restore = consumePendingScrollRestore() if (restore) { scrollDocumentTo(restore) } else if (!route.hash) { scrollDocumentToTop() } await new Promise((resolve) => requestAnimationFrame(resolve)) await new Promise((resolve) => requestAnimationFrame(resolve)) if (restore) { scrollDocumentTo(restore) } else if (!route.hash) { scrollDocumentToTop() } await new Promise((resolve) => setTimeout(resolve, 48)) hideRouteVeil() } if (import.meta.client && 'scrollRestoration' in window.history) { window.history.scrollRestoration = 'manual' } document.addEventListener('click', handleInternalLinkCapture, true) router.beforeEach((to, from) => { if (to.fullPath === from.fullPath) { return } if (!shouldUseSiteRouteVeil(to.path) && !shouldUseSiteRouteVeil(from.path)) { return } showRouteVeil() }) router.onError(() => { hideRouteVeil() }) nuxtApp.hook('page:finish', () => { finishRouteTransition() }) nuxtApp.hook('app:error', () => { hideRouteVeil() }) })