From: Eduardo San Martin Morote Date: Mon, 5 Aug 2019 18:45:54 +0000 (+0200) Subject: feat(scroll): add scrollBehavior for html5 history X-Git-Tag: v4.0.0-alpha.0~283 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=987a4413ac7ae10589f888b1f2d06ae117190983;p=thirdparty%2Fvuejs%2Frouter.git feat(scroll): add scrollBehavior for html5 history --- diff --git a/explorations/html5.html b/explorations/html5.html index 00b19163..9aba461c 100644 --- a/explorations/html5.html +++ b/explorations/html5.html @@ -6,6 +6,35 @@ Testing History HTML5 + +
@@ -18,6 +47,9 @@
  • /
  • +
  • + /long-0 +
  • /users/5
  • @@ -31,7 +63,14 @@ Doc with same id --> - + + +
    diff --git a/explorations/html5.ts b/explorations/html5.ts index 19128c0c..f872c78a 100644 --- a/explorations/html5.ts +++ b/explorations/html5.ts @@ -37,6 +37,19 @@ const User: RouteComponent = { template: `
    User: {{ $route.params.id }}
    `, } +const LongView: RouteComponent = { + template: ` +
    +
    This one is long: {{ $route.params.n }}. Go down to click on a link
    +

    + /long-{{ Number($route.params.n || 0) + 1 }} +

    +
    + `, +} + const GuardedWithLeave: RouteComponent = { template: `

    try to leave

    @@ -47,8 +60,35 @@ const GuardedWithLeave: RouteComponent = { }, } -// 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 | 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: [ @@ -57,6 +97,7 @@ const router = new Router({ { 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', @@ -78,6 +119,14 @@ const router = new Router({ }, // { path: /^\/about\/?$/, component }, ], + async scrollBehavior(to, from, savedPosition) { + await scrollWaiter.wait() + if (savedPosition) { + return savedPosition + } else { + return { x: 0, y: 0 } + } + }, }) // for testing purposes @@ -173,6 +222,15 @@ window.vm = new Vue({ shared, }, + methods: { + flushWaiter() { + scrollWaiter.flush() + }, + setupWaiter() { + scrollWaiter.add() + }, + }, + // try out watchers // watch: { // '$route.params.id' (id) { diff --git a/src/history/html5.ts b/src/history/html5.ts index 0fb5a702..5dc32b01 100644 --- a/src/history/html5.ts +++ b/src/history/html5.ts @@ -1,6 +1,7 @@ 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') @@ -16,6 +17,7 @@ interface StateEntry { current: HistoryLocationNormalized forward: HistoryLocationNormalized | null replaced: boolean + scroll: ScrollToPosition } // TODO: pretty useless right now except for typing @@ -30,6 +32,7 @@ function buildState( current, forward, replaced, + scroll: computeScrollPosition(), } } diff --git a/src/router.ts b/src/router.ts index f6c69ba5..a9f383ea 100644 --- a/src/router.ts +++ b/src/router.ts @@ -17,6 +17,11 @@ import { MatcherLocation, RouteQueryAndHash, } from './types/index' +import { + ScrollToPosition, + ScrollPosition, + scrollToPosition, +} from './utils/scroll' import { guardToPromiseFn, extractComponentsGuards } from './utils' import { @@ -25,9 +30,19 @@ import { NavigationCancelled, } from './errors' +interface ScrollBehavior { + ( + to: RouteLocationNormalized, + from: RouteLocationNormalized, + savedPosition: ScrollToPosition + ): ScrollPosition | Promise +} + export interface RouterOptions { history: BaseHistory routes: RouteRecord[] + // TODO: async version + scrollBehavior?: ScrollBehavior } type ErrorHandler = (error: any) => any @@ -46,10 +61,12 @@ export class Router { 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) @@ -77,6 +94,7 @@ export class Router { ...matchedRoute, } this.updateReactiveRoute() + this.handleScroll(toLocation, this.pendingLocation) } catch (error) { if (NavigationGuardRedirect.is(error)) { // TODO: refactor the duplication of new NavigationCancelled by @@ -112,7 +130,6 @@ export class Router { }) } - // TODO: rename to resolveLocation? resolveLocation( location: MatcherLocation & Required, currentLocation: RouteLocationNormalized, @@ -491,4 +508,21 @@ export class Router { 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) + } } diff --git a/src/utils/scroll.ts b/src/utils/scroll.ts new file mode 100644 index 00000000..8876d1e9 --- /dev/null +++ b/src/utils/scroll.ts @@ -0,0 +1,67 @@ +// 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) + } +}