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
+
+
/
+
/foo
+
/bar
+
/bar#anchor
+
/bar#anchor2
+
+
+
+ `
+}).$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()
+ }
+}