Skip to content

Commit 1422eb5

Browse files
ariesjiayyx990803
authored andcommitted
feat: enhance hashHistory to support scrollBehavior (#1662)
* enhance hashhistory to support scrollbehavior * fix ensure slash
1 parent ce13b55 commit 1422eb5

File tree

4 files changed

+187
-7
lines changed

4 files changed

+187
-7
lines changed

examples/hash-scroll-behavior/app.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import Vue from 'vue'
2+
import VueRouter from 'vue-router'
3+
4+
Vue.use(VueRouter)
5+
6+
const Home = { template: '<div>home</div>' }
7+
const Foo = { template: '<div>foo</div>' }
8+
const Bar = {
9+
template: `
10+
<div>
11+
bar
12+
<div style="height:500px"></div>
13+
<p id="anchor" style="height:500px">Anchor</p>
14+
<p id="anchor2">Anchor2</p>
15+
</div>
16+
`
17+
}
18+
19+
// scrollBehavior:
20+
// - only available in html5 history mode
21+
// - defaults to no scroll behavior
22+
// - return false to prevent scroll
23+
const scrollBehavior = (to, from, savedPosition) => {
24+
if (savedPosition) {
25+
// savedPosition is only available for popstate navigations.
26+
return savedPosition
27+
} else {
28+
const position = {}
29+
// new navigation.
30+
// scroll to anchor by returning the selector
31+
if (to.hash) {
32+
position.selector = to.hash
33+
console.log(to)
34+
35+
// specify offset of the element
36+
if (to.hash === '#anchor2') {
37+
position.offset = { y: 100 }
38+
}
39+
}
40+
// check if any matched route config has meta that requires scrolling to top
41+
if (to.matched.some(m => m.meta.scrollToTop)) {
42+
// cords will be used if no selector is provided,
43+
// or if the selector didn't match any element.
44+
position.x = 0
45+
position.y = 0
46+
}
47+
// if the returned position is falsy or an empty object,
48+
// will retain current scroll position.
49+
return position
50+
}
51+
}
52+
53+
const router = new VueRouter({
54+
mode: 'hash',
55+
scrollBehavior,
56+
routes: [
57+
{ path: '/', component: Home, meta: { scrollToTop: true }},
58+
{ path: '/foo', component: Foo },
59+
{ path: '/bar', component: Bar, meta: { scrollToTop: true }}
60+
]
61+
})
62+
63+
new Vue({
64+
router,
65+
template: `
66+
<div id="app">
67+
<h1>Scroll Behavior</h1>
68+
<ul>
69+
<li><router-link to="/">/</router-link></li>
70+
<li><router-link to="/foo">/foo</router-link></li>
71+
<li><router-link to="/bar">/bar</router-link></li>
72+
<li><router-link to="/bar#anchor">/bar#anchor</router-link></li>
73+
<li><router-link to="/bar#anchor2">/bar#anchor2</router-link></li>
74+
</ul>
75+
<router-view class="view"></router-view>
76+
</div>
77+
`
78+
}).$mount('#app')
+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<link rel="stylesheet" href="/global.css">
3+
<style>
4+
.view {
5+
border: 1px solid red;
6+
height: 2000px;
7+
position: relative;
8+
}
9+
</style>
10+
<a href="/">&larr; Examples index</a>
11+
<div id="app"></div>
12+
<script src="/__build__/shared.js"></script>
13+
<script src="/__build__/hash-scroll-behavior.js"></script>

src/history/hash.js

+39-7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import type Router from '../index'
44
import { History } from './base'
55
import { cleanPath } from '../util/path'
66
import { getLocation } from './html5'
7+
import { setupScroll, handleScroll } from '../util/scroll'
8+
import { pushState, replaceState, supportsPushState } from '../util/push-state'
79

810
export class HashHistory extends History {
911
constructor (router: Router, base: ?string, fallback: boolean) {
@@ -18,26 +20,44 @@ export class HashHistory extends History {
1820
// this is delayed until the app mounts
1921
// to avoid the hashchange listener being fired too early
2022
setupListeners () {
21-
window.addEventListener('hashchange', () => {
23+
const router = this.router
24+
const expectScroll = router.options.scrollBehavior
25+
const supportsScroll = supportsPushState && expectScroll
26+
27+
if (supportsScroll) {
28+
setupScroll()
29+
}
30+
31+
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
32+
const current = this.current
2233
if (!ensureSlash()) {
2334
return
2435
}
2536
this.transitionTo(getHash(), route => {
26-
replaceHash(route.fullPath)
37+
if (supportsScroll) {
38+
handleScroll(this.router, route, current, true)
39+
}
40+
if (!supportsPushState) {
41+
replaceHash(route.fullPath)
42+
}
2743
})
2844
})
2945
}
3046

3147
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
48+
const { current: fromRoute } = this
3249
this.transitionTo(location, route => {
3350
pushHash(route.fullPath)
51+
handleScroll(this.router, route, fromRoute, false)
3452
onComplete && onComplete(route)
3553
}, onAbort)
3654
}
3755

3856
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
57+
const { current: fromRoute } = this
3958
this.transitionTo(location, route => {
4059
replaceHash(route.fullPath)
60+
handleScroll(this.router, route, fromRoute, false)
4161
onComplete && onComplete(route)
4262
}, onAbort)
4363
}
@@ -85,13 +105,25 @@ export function getHash (): string {
85105
return index === -1 ? '' : href.slice(index + 1)
86106
}
87107

108+
function getUrl (path) {
109+
const href = window.location.href
110+
const i = href.indexOf('#')
111+
const base = i >= 0 ? href.slice(0, i) : href
112+
return `${base}#${path}`
113+
}
114+
88115
function pushHash (path) {
89-
window.location.hash = path
116+
if (supportsPushState) {
117+
pushState(getUrl(path))
118+
} else {
119+
window.location.hash = path
120+
}
90121
}
91122

92123
function replaceHash (path) {
93-
const href = window.location.href
94-
const i = href.indexOf('#')
95-
const base = i >= 0 ? href.slice(0, i) : href
96-
window.location.replace(`${base}#${path}`)
124+
if (supportsPushState) {
125+
replaceState(getUrl(path))
126+
} else {
127+
window.location.replace(getUrl(path))
128+
}
97129
}
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
module.exports = {
2+
'scroll behavior': function (browser) {
3+
browser
4+
.url('http://localhost:8080/hash-scroll-behavior/')
5+
.waitForElementVisible('#app', 1000)
6+
.assert.count('li a', 5)
7+
.assert.containsText('.view', 'home')
8+
9+
.execute(function () {
10+
window.scrollTo(0, 100)
11+
})
12+
.click('li:nth-child(2) a')
13+
.assert.containsText('.view', 'foo')
14+
.execute(function () {
15+
window.scrollTo(0, 200)
16+
window.history.back()
17+
})
18+
.assert.containsText('.view', 'home')
19+
.assert.evaluate(function () {
20+
return window.pageYOffset === 100
21+
}, null, 'restore scroll position on back')
22+
23+
// scroll on a popped entry
24+
.execute(function () {
25+
window.scrollTo(0, 50)
26+
window.history.forward()
27+
})
28+
.assert.containsText('.view', 'foo')
29+
.assert.evaluate(function () {
30+
return window.pageYOffset === 200
31+
}, null, 'restore scroll position on forward')
32+
33+
.execute(function () {
34+
window.history.back()
35+
})
36+
.assert.containsText('.view', 'home')
37+
.assert.evaluate(function () {
38+
return window.pageYOffset === 50
39+
}, null, 'restore scroll position on back again')
40+
41+
.click('li:nth-child(3) a')
42+
.assert.evaluate(function () {
43+
return window.pageYOffset === 0
44+
}, null, 'scroll to top on new entry')
45+
46+
.click('li:nth-child(4) a')
47+
.assert.evaluate(function () {
48+
return document.getElementById('anchor').getBoundingClientRect().top < 1
49+
}, null, 'scroll to anchor')
50+
51+
.click('li:nth-child(5) a')
52+
.assert.evaluate(function () {
53+
return document.getElementById('anchor2').getBoundingClientRect().top < 101
54+
}, null, 'scroll to anchor with offset')
55+
.end()
56+
}
57+
}

0 commit comments

Comments
 (0)