diff --git a/flow/declarations.js b/flow/declarations.js index 942aa73bf..cceafe1f3 100644 --- a/flow/declarations.js +++ b/flow/declarations.js @@ -28,7 +28,7 @@ declare type NavigationGuard = ( declare type AfterNavigationHook = (to: Route, from: Route) => any type Position = { x: number, y: number }; -type PositionResult = Position | { selector: string, offset?: Position } | void; +type PositionResult = Position | { selector: string, offset?: Position, scrollElement?: string } | void; declare type RouterOptions = { routes?: Array; @@ -39,6 +39,7 @@ declare type RouterOptions = { linkExactActiveClass?: string; parseQuery?: (query: string) => Object; stringifyQuery?: (query: Object) => string; + scrollElement?: string; scrollBehavior?: ( to: Route, from: Route, diff --git a/src/history/base.js b/src/history/base.js index fbdf25696..25713cc84 100644 --- a/src/history/base.js +++ b/src/history/base.js @@ -84,6 +84,10 @@ export class History { }) } + get scrollElementSelector (): ?string { + return this.router.options.scrollElement + } + confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { const current = this.current const abort = err => { diff --git a/src/history/hash.js b/src/history/hash.js index 16c94db0e..9ac9db741 100644 --- a/src/history/hash.js +++ b/src/history/hash.js @@ -25,7 +25,7 @@ export class HashHistory extends History { const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { - setupScroll() + setupScroll(router.options.scrollElement) } window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => { @@ -38,7 +38,7 @@ export class HashHistory extends History { handleScroll(this.router, route, current, true) } if (!supportsPushState) { - replaceHash(route.fullPath) + replaceHash(route.fullPath, this.scrollElementSelector) } }) }) @@ -47,7 +47,7 @@ export class HashHistory extends History { push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { - pushHash(route.fullPath) + pushHash(route.fullPath, this.scrollElementSelector) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) @@ -56,7 +56,7 @@ export class HashHistory extends History { replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { - replaceHash(route.fullPath) + replaceHash(route.fullPath, this.scrollElementSelector) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) @@ -69,7 +69,7 @@ export class HashHistory extends History { ensureURL (push?: boolean) { const current = this.current.fullPath if (getHash() !== current) { - push ? pushHash(current) : replaceHash(current) + push ? pushHash(current, this.scrollElementSelector) : replaceHash(current, this.scrollElementSelector) } } @@ -128,17 +128,17 @@ function getUrl (path) { return `${base}#${path}` } -function pushHash (path) { +function pushHash (path, scrollElementSelector?: string) { if (supportsPushState) { - pushState(getUrl(path)) + pushState(getUrl(path,), scrollElementSelector) } else { window.location.hash = path } } -function replaceHash (path) { +function replaceHash (path, scrollElementSelector?: string) { if (supportsPushState) { - replaceState(getUrl(path)) + replaceState(getUrl(path), scrollElementSelector) } else { window.location.replace(getUrl(path)) } diff --git a/src/history/html5.js b/src/history/html5.js index e1cdba97d..e6f28d505 100644 --- a/src/history/html5.js +++ b/src/history/html5.js @@ -44,7 +44,7 @@ export class HTML5History extends History { push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { - pushState(cleanPath(this.base + route.fullPath)) + pushState(cleanPath(this.base + route.fullPath), this.scrollElementSelector) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) @@ -53,7 +53,7 @@ export class HTML5History extends History { replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { - replaceState(cleanPath(this.base + route.fullPath)) + replaceState(cleanPath(this.base + route.fullPath), this.scrollElementSelector) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) @@ -62,7 +62,7 @@ export class HTML5History extends History { ensureURL (push?: boolean) { if (getLocation(this.base) !== this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) - push ? pushState(current) : replaceState(current) + push ? pushState(current, this.scrollElementSelector) : replaceState(current, this.scrollElementSelector) } } diff --git a/src/util/push-state.js b/src/util/push-state.js index 136d6f9ec..b216f69a0 100644 --- a/src/util/push-state.js +++ b/src/util/push-state.js @@ -37,8 +37,8 @@ export function setStateKey (key: string) { _key = key } -export function pushState (url?: string, replace?: boolean) { - saveScrollPosition() +export function pushState (url?: string, scrollElementSelector?: ?string, replace?: boolean) { + saveScrollPosition(scrollElementSelector) // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history @@ -54,6 +54,6 @@ export function pushState (url?: string, replace?: boolean) { } } -export function replaceState (url?: string) { - pushState(url, true) +export function replaceState (url?: string, scrollElementSelector?: ?string) { + pushState(url, scrollElementSelector, true) } diff --git a/src/util/scroll.js b/src/util/scroll.js index 1f4e68f30..6b90cd3bd 100644 --- a/src/util/scroll.js +++ b/src/util/scroll.js @@ -6,12 +6,12 @@ import { getStateKey, setStateKey } from './push-state' const positionStore = Object.create(null) -export function setupScroll () { +export function setupScroll (scrollElement?: ?string) { // Fix for #1585 for Firefox // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678 window.history.replaceState({ key: getStateKey() }, '', window.location.href.replace(window.location.origin, '')) window.addEventListener('popstate', e => { - saveScrollPosition() + saveScrollPosition(scrollElement) if (e.state && e.state.key) { setStateKey(e.state.key) } @@ -48,25 +48,59 @@ export function handleScroll ( if (typeof shouldScroll.then === 'function') { shouldScroll.then(shouldScroll => { - scrollToPosition((shouldScroll: any), position) + if (!shouldScroll) { + return + } + const scrollElementSelector = router.options.scrollElement || shouldScroll.scrollElement || null + scrollToPosition((shouldScroll: any), position, scrollElementSelector) }).catch(err => { if (process.env.NODE_ENV !== 'production') { assert(false, err.toString()) } }) } else { - scrollToPosition(shouldScroll, position) + const scrollElementSelector = router.options.scrollElement || shouldScroll.scrollElement || null + scrollToPosition(shouldScroll, position, scrollElementSelector) } }) } -export function saveScrollPosition () { - const key = getStateKey() - if (key) { - positionStore[key] = { - x: window.pageXOffset, - y: window.pageYOffset +function getScrollElement (scrollElementSelector?: any): Element | WindowProxy { + let domScrollElement = window + if (!scrollElementSelector) { + return window + } + + assert(typeof scrollElementSelector === 'string' || scrollElementSelector instanceof Element, 'Scroll Element must be a css selector string or a DOM element') + if (typeof scrollElementSelector === 'string') { + const customScrollElement = document.querySelector(scrollElementSelector) + if (customScrollElement) { + domScrollElement = customScrollElement } + } else if (scrollElementSelector instanceof Element) { + return scrollElementSelector + } + + return domScrollElement +} + +export function saveScrollPosition (scrollElement?: string | Element | null) { + const key = getStateKey() + if (!key) { + return + } + + const domScrollElement = getScrollElement(scrollElement) + let propX = 'pageXOffset' + let propY = 'pageYOffset' + if (domScrollElement instanceof Element) { + propX = 'scrollLeft' + propY = 'scrollTop' + } + + positionStore[key] = { + x: domScrollElement[propX], + y: domScrollElement[propY] } } @@ -109,7 +143,7 @@ function isNumber (v: any): boolean { return typeof v === 'number' } -function scrollToPosition (shouldScroll, position) { +function scrollToPosition (shouldScroll, position, scrollElementSelector?) { const isObject = typeof shouldScroll === 'object' if (isObject && typeof shouldScroll.selector === 'string') { const el = document.querySelector(shouldScroll.selector) @@ -125,6 +159,12 @@ function scrollToPosition (shouldScroll, position) { } if (position) { - window.scrollTo(position.x, position.y) + const scrollElement = getScrollElement(scrollElementSelector) + if (typeof scrollElement.scrollTo === 'function') { + scrollElement.scrollTo(position.x, position.y) + } else { + scrollElement.scrollTop = position.y + scrollElement.scrollLeft = position.x + } } }