<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Testing History HTML5</title>
<script src="https://polyfill.io/v3/polyfill.min.js?features=default%2Ces2015"></script>
+
+ <style>
+ .long {
+ background-color: lightgray;
+ height: 3000px;
+ }
+ .fade-enter-active,
+ .fade-leave-active {
+ transition: opacity 0.15s ease;
+ }
+ .fade-enter,
+ .fade-leave-active {
+ opacity: 0;
+ }
+ .child-view {
+ position: absolute;
+ transition: all 0.3s cubic-bezier(0.55, 0, 0.1, 1);
+ }
+ .slide-left-enter,
+ .slide-right-leave-active {
+ opacity: 0;
+ transform: translate(30px, 0);
+ }
+ .slide-left-leave-active,
+ .slide-right-enter {
+ opacity: 0;
+ transform: translate(-30px, 0);
+ }
+ </style>
</head>
<body>
<div id="app">
<li>
<router-link to="/">/</router-link>
</li>
+ <li>
+ <router-link to="/long-0">/long-0</router-link>
+ </li>
<li>
<router-link to="/users/5">/users/5</router-link>
</li>
<router-link :to="{ name: 'docs' }">Doc with same id</router-link>
</li> -->
</ul>
- <router-view></router-view>
+ <transition
+ name="fade"
+ mode="out-in"
+ @after-enter="flushWaiter"
+ @before-leave="setupWaiter"
+ >
+ <router-view></router-view>
+ </transition>
</div>
</body>
</html>
template: `<div>User: {{ $route.params.id }}</div>`,
}
+const LongView: RouteComponent = {
+ template: `
+ <section>
+ <div class="long">This one is long: {{ $route.params.n }}. Go down to click on a link</div>
+ <p class="long">
+ <router-link
+ :to="{ name: 'long', params: { n: Number($route.params.n || 0) + 1 }}"
+ >/long-{{ Number($route.params.n || 0) + 1 }}</router-link>
+ </p>
+ </section>
+ `,
+}
+
const GuardedWithLeave: RouteComponent = {
template: `<div>
<p>try to leave</p>
},
}
-// const hist = new HTML5History()
-const hist = new HashHistory()
+if ('scrollRestoration' in history) {
+ history.scrollRestoration = 'manual'
+}
+
+class ScrollQueue {
+ private resolve: (() => void) | null = null
+ private promise: Promise<any> | null = null
+
+ add() {
+ this.promise = new Promise(resolve => {
+ this.resolve = resolve
+ })
+ }
+
+ flush() {
+ this.resolve && this.resolve()
+ this.resolve = null
+ this.promise = null
+ }
+
+ async wait() {
+ await this.promise
+ }
+}
+
+const scrollWaiter = new ScrollQueue()
+
+const hist = new HTML5History()
+// const hist = new HashHistory()
const router = new Router({
history: hist,
routes: [
{ path: '/documents/:id', name: 'docs', component: User },
{ path: '/n/:n', name: 'increment', component },
{ path: '/multiple/:a/:b', name: 'multiple', component },
+ { path: '/long-:n', name: 'long', component: LongView },
{
path: '/with-guard/:n',
name: 'guarded',
},
// { path: /^\/about\/?$/, component },
],
+ async scrollBehavior(to, from, savedPosition) {
+ await scrollWaiter.wait()
+ if (savedPosition) {
+ return savedPosition
+ } else {
+ return { x: 0, y: 0 }
+ }
+ },
})
// for testing purposes
shared,
},
+ methods: {
+ flushWaiter() {
+ scrollWaiter.flush()
+ },
+ setupWaiter() {
+ scrollWaiter.add()
+ },
+ },
+
// try out watchers
// watch: {
// '$route.params.id' (id) {
import consola from '../consola'
import { BaseHistory, HistoryLocationNormalized, HistoryLocation } from './base'
import { NavigationCallback, HistoryState, NavigationDirection } from './base'
+import { computeScrollPosition, ScrollToPosition } from '../utils/scroll'
const cs = consola.withTag('html5')
current: HistoryLocationNormalized
forward: HistoryLocationNormalized | null
replaced: boolean
+ scroll: ScrollToPosition
}
// TODO: pretty useless right now except for typing
current,
forward,
replaced,
+ scroll: computeScrollPosition(),
}
}
MatcherLocation,
RouteQueryAndHash,
} from './types/index'
+import {
+ ScrollToPosition,
+ ScrollPosition,
+ scrollToPosition,
+} from './utils/scroll'
import { guardToPromiseFn, extractComponentsGuards } from './utils'
import {
NavigationCancelled,
} from './errors'
+interface ScrollBehavior {
+ (
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalized,
+ savedPosition: ScrollToPosition
+ ): ScrollPosition | Promise<ScrollPosition>
+}
+
export interface RouterOptions {
history: BaseHistory
routes: RouteRecord[]
+ // TODO: async version
+ scrollBehavior?: ScrollBehavior
}
type ErrorHandler = (error: any) => any
private errorHandlers: ErrorHandler[] = []
private ready: boolean = false
private onReadyCbs: OnReadyCallback[] = []
+ private scrollBehavior?: ScrollBehavior
constructor(options: RouterOptions) {
this.history = options.history
// this.history.ensureLocation()
+ this.scrollBehavior = options.scrollBehavior
this.matcher = new RouterMatcher(options.routes)
...matchedRoute,
}
this.updateReactiveRoute()
+ this.handleScroll(toLocation, this.pendingLocation)
} catch (error) {
if (NavigationGuardRedirect.is(error)) {
// TODO: refactor the duplication of new NavigationCancelled by
})
}
- // TODO: rename to resolveLocation?
resolveLocation(
location: MatcherLocation & Required<RouteQueryAndHash>,
currentLocation: RouteLocationNormalized,
return this.currentRoute
}
+
+ private async handleScroll(
+ to: RouteLocationNormalized,
+ from: RouteLocationNormalized
+ ) {
+ if (!this.scrollBehavior) return
+ // TODO: handle other histories
+ const { state } = window.history
+ if (!state) return
+ const scroll: ScrollToPosition | void = state.scroll
+ if (!scroll) return
+
+ await this.app.$nextTick()
+ const position = await this.scrollBehavior(to, from, scroll)
+ console.log('scrolling to', position)
+ scrollToPosition(position)
+ }
}
--- /dev/null
+// import { RouteLocationNormalized } from '../types'
+
+export interface ScrollToPosition {
+ x: number
+ y: number
+}
+
+export interface ScrollToElement {
+ selector: string
+ offset?: ScrollToPosition
+}
+
+export type ScrollPosition = ScrollToPosition | ScrollToElement
+
+export function computeScrollPosition(el?: Element): ScrollToPosition {
+ return el
+ ? {
+ x: el.scrollLeft,
+ y: el.scrollTop,
+ }
+ : {
+ x: window.pageXOffset,
+ y: window.pageYOffset,
+ }
+}
+
+function getElementPosition(
+ el: Element,
+ offset: ScrollToPosition
+): ScrollToPosition {
+ const docEl = document.documentElement
+ const docRect = docEl.getBoundingClientRect()
+ const elRect = el.getBoundingClientRect()
+ return {
+ x: elRect.left - docRect.left - offset.x,
+ y: elRect.top - docRect.top - offset.y,
+ }
+}
+
+const hashStartsWithNumberRE = /^#\d/
+
+export function scrollToPosition(position: ScrollPosition): void {
+ let normalizedPosition: ScrollToPosition | null = null
+
+ if ('selector' in position) {
+ // getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
+ // but at the same time, it doesn't make much sense to select an element with an id and an extra selector
+ const el = hashStartsWithNumberRE.test(position.selector)
+ ? document.getElementById(position.selector.slice(1))
+ : document.querySelector(position.selector)
+
+ if (el) {
+ const offset: ScrollToPosition = position.offset || { x: 0, y: 0 }
+ normalizedPosition = getElementPosition(el, offset)
+ }
+ // TODO: else dev warning?
+ } else {
+ normalizedPosition = {
+ x: position.x,
+ y: position.y,
+ }
+ }
+
+ if (normalizedPosition) {
+ window.scrollTo(normalizedPosition.x, normalizedPosition.y)
+ }
+}