From 1422eb51584139d210a2065d1d384e36cd73b9de Mon Sep 17 00:00:00 2001 From: Chenjia Date: Wed, 11 Oct 2017 15:04:09 -0500 Subject: [PATCH] feat: enhance hashHistory to support scrollBehavior (#1662) * enhance hashhistory to support scrollbehavior * fix ensure slash --- examples/hash-scroll-behavior/app.js | 78 ++++++++++++++++++++++++ examples/hash-scroll-behavior/index.html | 13 ++++ src/history/hash.js | 46 +++++++++++--- test/e2e/specs/hash-scroll-behavior.js | 57 +++++++++++++++++ 4 files changed, 187 insertions(+), 7 deletions(-) create mode 100644 examples/hash-scroll-behavior/app.js create mode 100644 examples/hash-scroll-behavior/index.html create mode 100644 test/e2e/specs/hash-scroll-behavior.js diff --git a/examples/hash-scroll-behavior/app.js b/examples/hash-scroll-behavior/app.js new file mode 100644 index 000000000..0f1667e79 --- /dev/null +++ b/examples/hash-scroll-behavior/app.js @@ -0,0 +1,78 @@ +import Vue from 'vue' +import VueRouter from 'vue-router' + +Vue.use(VueRouter) + +const Home = { template: '
home
' } +const Foo = { template: '
foo
' } +const Bar = { + template: ` +
+ bar +
+

Anchor

+

Anchor2

+
+ ` +} + +// scrollBehavior: +// - only available in html5 history mode +// - defaults to no scroll behavior +// - return false to prevent scroll +const scrollBehavior = (to, from, savedPosition) => { + if (savedPosition) { + // savedPosition is only available for popstate navigations. + return savedPosition + } else { + const position = {} + // new navigation. + // scroll to anchor by returning the selector + if (to.hash) { + position.selector = to.hash + console.log(to) + + // specify offset of the element + if (to.hash === '#anchor2') { + position.offset = { y: 100 } + } + } + // check if any matched route config has meta that requires scrolling to top + if (to.matched.some(m => m.meta.scrollToTop)) { + // cords will be used if no selector is provided, + // or if the selector didn't match any element. + position.x = 0 + position.y = 0 + } + // if the returned position is falsy or an empty object, + // will retain current scroll position. + return position + } +} + +const router = new VueRouter({ + mode: 'hash', + scrollBehavior, + routes: [ + { path: '/', component: Home, meta: { scrollToTop: true }}, + { path: '/foo', component: Foo }, + { path: '/bar', component: Bar, meta: { scrollToTop: true }} + ] +}) + +new Vue({ + router, + template: ` +
+

Scroll Behavior

+ + +
+ ` +}).$mount('#app') diff --git a/examples/hash-scroll-behavior/index.html b/examples/hash-scroll-behavior/index.html new file mode 100644 index 000000000..2f20163ed --- /dev/null +++ b/examples/hash-scroll-behavior/index.html @@ -0,0 +1,13 @@ + + + +← Examples index +
+ + diff --git a/src/history/hash.js b/src/history/hash.js index a79319eda..f4a2f3828 100644 --- a/src/history/hash.js +++ b/src/history/hash.js @@ -4,6 +4,8 @@ import type Router from '../index' import { History } from './base' import { cleanPath } from '../util/path' import { getLocation } from './html5' +import { setupScroll, handleScroll } from '../util/scroll' +import { pushState, replaceState, supportsPushState } from '../util/push-state' export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { @@ -18,26 +20,44 @@ export class HashHistory extends History { // this is delayed until the app mounts // to avoid the hashchange listener being fired too early setupListeners () { - window.addEventListener('hashchange', () => { + const router = this.router + const expectScroll = router.options.scrollBehavior + const supportsScroll = supportsPushState && expectScroll + + if (supportsScroll) { + setupScroll() + } + + window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { + const current = this.current if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { - replaceHash(route.fullPath) + if (supportsScroll) { + handleScroll(this.router, route, current, true) + } + if (!supportsPushState) { + replaceHash(route.fullPath) + } }) }) } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const { current: fromRoute } = this this.transitionTo(location, route => { pushHash(route.fullPath) + handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { + const { current: fromRoute } = this this.transitionTo(location, route => { replaceHash(route.fullPath) + handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } @@ -85,13 +105,25 @@ export function getHash (): string { return index === -1 ? '' : href.slice(index + 1) } +function getUrl (path) { + const href = window.location.href + const i = href.indexOf('#') + const base = i >= 0 ? href.slice(0, i) : href + return `${base}#${path}` +} + function pushHash (path) { - window.location.hash = path + if (supportsPushState) { + pushState(getUrl(path)) + } else { + window.location.hash = path + } } function replaceHash (path) { - const href = window.location.href - const i = href.indexOf('#') - const base = i >= 0 ? href.slice(0, i) : href - window.location.replace(`${base}#${path}`) + if (supportsPushState) { + replaceState(getUrl(path)) + } else { + window.location.replace(getUrl(path)) + } } diff --git a/test/e2e/specs/hash-scroll-behavior.js b/test/e2e/specs/hash-scroll-behavior.js new file mode 100644 index 000000000..846956333 --- /dev/null +++ b/test/e2e/specs/hash-scroll-behavior.js @@ -0,0 +1,57 @@ +module.exports = { + 'scroll behavior': function (browser) { + browser + .url('http://localhost:8080/hash-scroll-behavior/') + .waitForElementVisible('#app', 1000) + .assert.count('li a', 5) + .assert.containsText('.view', 'home') + + .execute(function () { + window.scrollTo(0, 100) + }) + .click('li:nth-child(2) a') + .assert.containsText('.view', 'foo') + .execute(function () { + window.scrollTo(0, 200) + window.history.back() + }) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 100 + }, null, 'restore scroll position on back') + + // scroll on a popped entry + .execute(function () { + window.scrollTo(0, 50) + window.history.forward() + }) + .assert.containsText('.view', 'foo') + .assert.evaluate(function () { + return window.pageYOffset === 200 + }, null, 'restore scroll position on forward') + + .execute(function () { + window.history.back() + }) + .assert.containsText('.view', 'home') + .assert.evaluate(function () { + return window.pageYOffset === 50 + }, null, 'restore scroll position on back again') + + .click('li:nth-child(3) a') + .assert.evaluate(function () { + return window.pageYOffset === 0 + }, null, 'scroll to top on new entry') + + .click('li:nth-child(4) a') + .assert.evaluate(function () { + return document.getElementById('anchor').getBoundingClientRect().top < 1 + }, null, 'scroll to anchor') + + .click('li:nth-child(5) a') + .assert.evaluate(function () { + return document.getElementById('anchor2').getBoundingClientRect().top < 101 + }, null, 'scroll to anchor with offset') + .end() + } +}