Skip to content

Commit

Permalink
feat: enhance hashHistory to support scrollBehavior (#1662)
Browse files Browse the repository at this point in the history
* enhance hashhistory to support scrollbehavior

* fix ensure slash
  • Loading branch information
ariesjia authored and yyx990803 committed Oct 11, 2017
1 parent ce13b55 commit 1422eb5
Show file tree
Hide file tree
Showing 4 changed files with 187 additions and 7 deletions.
78 changes: 78 additions & 0 deletions examples/hash-scroll-behavior/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const Home = { template: '<div>home</div>' }
const Foo = { template: '<div>foo</div>' }
const Bar = {
template: `
<div>
bar
<div style="height:500px"></div>
<p id="anchor" style="height:500px">Anchor</p>
<p id="anchor2">Anchor2</p>
</div>
`
}

// 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: `
<div id="app">
<h1>Scroll Behavior</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/foo">/foo</router-link></li>
<li><router-link to="/bar">/bar</router-link></li>
<li><router-link to="/bar#anchor">/bar#anchor</router-link></li>
<li><router-link to="/bar#anchor2">/bar#anchor2</router-link></li>
</ul>
<router-view class="view"></router-view>
</div>
`
}).$mount('#app')
13 changes: 13 additions & 0 deletions examples/hash-scroll-behavior/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<link rel="stylesheet" href="/global.css">
<style>
.view {
border: 1px solid red;
height: 2000px;
position: relative;
}
</style>
<a href="/">&larr; Examples index</a>
<div id="app"></div>
<script src="/__build__/shared.js"></script>
<script src="/__build__/hash-scroll-behavior.js"></script>
46 changes: 39 additions & 7 deletions src/history/hash.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
Expand Down Expand Up @@ -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))
}
}
57 changes: 57 additions & 0 deletions test/e2e/specs/hash-scroll-behavior.js
Original file line number Diff line number Diff line change
@@ -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()
}
}

0 comments on commit 1422eb5

Please sign in to comment.