kdu-router 3.5.4 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +11 -11
- package/dist/composables.js +253 -0
- package/dist/composables.mjs +244 -0
- package/dist/kdu-router.common.js +2668 -2665
- package/dist/kdu-router.esm.browser.js +2676 -2670
- package/dist/kdu-router.esm.browser.min.js +5 -5
- package/dist/kdu-router.esm.js +2680 -2674
- package/dist/kdu-router.js +2675 -2672
- package/dist/kdu-router.min.js +5 -5
- package/ketur/attributes.json +38 -38
- package/ketur/tags.json +20 -20
- package/package.json +115 -90
- package/src/components/link.js +224 -224
- package/src/components/view.js +155 -155
- package/src/composables/globals.js +34 -0
- package/src/composables/guards.js +68 -0
- package/src/composables/index.js +3 -0
- package/src/composables/useLink.js +113 -0
- package/src/composables/utils.js +11 -0
- package/src/create-matcher.js +226 -226
- package/src/create-route-map.js +220 -220
- package/src/entries/cjs.js +3 -0
- package/src/entries/esm.js +12 -0
- package/src/history/abstract.js +72 -72
- package/src/history/base.js +379 -379
- package/src/history/hash.js +152 -152
- package/src/history/html5.js +99 -99
- package/src/index.js +3 -293
- package/src/install.js +52 -52
- package/src/router.js +293 -0
- package/src/util/async.js +18 -18
- package/src/util/dom.js +3 -3
- package/src/util/errors.js +86 -86
- package/src/util/location.js +69 -69
- package/src/util/misc.js +6 -6
- package/src/util/params.js +37 -37
- package/src/util/path.js +74 -74
- package/src/util/push-state.js +46 -46
- package/src/util/query.js +113 -113
- package/src/util/resolve-components.js +109 -109
- package/src/util/route.js +151 -151
- package/src/util/scroll.js +175 -175
- package/src/util/state-key.js +22 -22
- package/src/util/warn.js +14 -14
- package/types/composables.d.ts +53 -0
- package/types/index.d.ts +25 -21
- package/types/kdu.d.ts +22 -22
- package/types/router.d.ts +564 -211
package/src/util/route.js
CHANGED
|
@@ -1,151 +1,151 @@
|
|
|
1
|
-
/* @flow */
|
|
2
|
-
|
|
3
|
-
import type KduRouter from '../index'
|
|
4
|
-
import { stringifyQuery } from './query'
|
|
5
|
-
|
|
6
|
-
const trailingSlashRE = /\/?$/
|
|
7
|
-
|
|
8
|
-
export function createRoute (
|
|
9
|
-
record: ?RouteRecord,
|
|
10
|
-
location: Location,
|
|
11
|
-
redirectedFrom?: ?Location,
|
|
12
|
-
router?: KduRouter
|
|
13
|
-
): Route {
|
|
14
|
-
const stringifyQuery = router && router.options.stringifyQuery
|
|
15
|
-
|
|
16
|
-
let query: any = location.query || {}
|
|
17
|
-
try {
|
|
18
|
-
query = clone(query)
|
|
19
|
-
} catch (e) {}
|
|
20
|
-
|
|
21
|
-
const route: Route = {
|
|
22
|
-
name: location.name || (record && record.name),
|
|
23
|
-
meta: (record && record.meta) || {},
|
|
24
|
-
path: location.path || '/',
|
|
25
|
-
hash: location.hash || '',
|
|
26
|
-
query,
|
|
27
|
-
params: location.params || {},
|
|
28
|
-
fullPath: getFullPath(location, stringifyQuery),
|
|
29
|
-
matched: record ? formatMatch(record) : []
|
|
30
|
-
}
|
|
31
|
-
if (redirectedFrom) {
|
|
32
|
-
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
|
|
33
|
-
}
|
|
34
|
-
return Object.freeze(route)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function clone (value) {
|
|
38
|
-
if (Array.isArray(value)) {
|
|
39
|
-
return value.map(clone)
|
|
40
|
-
} else if (value && typeof value === 'object') {
|
|
41
|
-
const res = {}
|
|
42
|
-
for (const key in value) {
|
|
43
|
-
res[key] = clone(value[key])
|
|
44
|
-
}
|
|
45
|
-
return res
|
|
46
|
-
} else {
|
|
47
|
-
return value
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// the starting route that represents the initial state
|
|
52
|
-
export const START = createRoute(null, {
|
|
53
|
-
path: '/'
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
|
|
57
|
-
const res = []
|
|
58
|
-
while (record) {
|
|
59
|
-
res.unshift(record)
|
|
60
|
-
record = record.parent
|
|
61
|
-
}
|
|
62
|
-
return res
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function getFullPath (
|
|
66
|
-
{ path, query = {}, hash = '' },
|
|
67
|
-
_stringifyQuery
|
|
68
|
-
): string {
|
|
69
|
-
const stringify = _stringifyQuery || stringifyQuery
|
|
70
|
-
return (path || '/') + stringify(query) + hash
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function isSameRoute (a: Route, b: ?Route, onlyPath: ?boolean): boolean {
|
|
74
|
-
if (b === START) {
|
|
75
|
-
return a === b
|
|
76
|
-
} else if (!b) {
|
|
77
|
-
return false
|
|
78
|
-
} else if (a.path && b.path) {
|
|
79
|
-
return a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && (onlyPath ||
|
|
80
|
-
a.hash === b.hash &&
|
|
81
|
-
isObjectEqual(a.query, b.query))
|
|
82
|
-
} else if (a.name && b.name) {
|
|
83
|
-
return (
|
|
84
|
-
a.name === b.name &&
|
|
85
|
-
(onlyPath || (
|
|
86
|
-
a.hash === b.hash &&
|
|
87
|
-
isObjectEqual(a.query, b.query) &&
|
|
88
|
-
isObjectEqual(a.params, b.params))
|
|
89
|
-
)
|
|
90
|
-
)
|
|
91
|
-
} else {
|
|
92
|
-
return false
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function isObjectEqual (a = {}, b = {}): boolean {
|
|
97
|
-
// handle null value #1566
|
|
98
|
-
if (!a || !b) return a === b
|
|
99
|
-
const aKeys = Object.keys(a).sort()
|
|
100
|
-
const bKeys = Object.keys(b).sort()
|
|
101
|
-
if (aKeys.length !== bKeys.length) {
|
|
102
|
-
return false
|
|
103
|
-
}
|
|
104
|
-
return aKeys.every((key, i) => {
|
|
105
|
-
const aVal = a[key]
|
|
106
|
-
const bKey = bKeys[i]
|
|
107
|
-
if (bKey !== key) return false
|
|
108
|
-
const bVal = b[key]
|
|
109
|
-
// query values can be null and undefined
|
|
110
|
-
if (aVal == null || bVal == null) return aVal === bVal
|
|
111
|
-
// check nested equality
|
|
112
|
-
if (typeof aVal === 'object' && typeof bVal === 'object') {
|
|
113
|
-
return isObjectEqual(aVal, bVal)
|
|
114
|
-
}
|
|
115
|
-
return String(aVal) === String(bVal)
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export function isIncludedRoute (current: Route, target: Route): boolean {
|
|
120
|
-
return (
|
|
121
|
-
current.path.replace(trailingSlashRE, '/').indexOf(
|
|
122
|
-
target.path.replace(trailingSlashRE, '/')
|
|
123
|
-
) === 0 &&
|
|
124
|
-
(!target.hash || current.hash === target.hash) &&
|
|
125
|
-
queryIncludes(current.query, target.query)
|
|
126
|
-
)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function queryIncludes (current: Dictionary<string>, target: Dictionary<string>): boolean {
|
|
130
|
-
for (const key in target) {
|
|
131
|
-
if (!(key in current)) {
|
|
132
|
-
return false
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return true
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function handleRouteEntered (route: Route) {
|
|
139
|
-
for (let i = 0; i < route.matched.length; i++) {
|
|
140
|
-
const record = route.matched[i]
|
|
141
|
-
for (const name in record.instances) {
|
|
142
|
-
const instance = record.instances[name]
|
|
143
|
-
const cbs = record.enteredCbs[name]
|
|
144
|
-
if (!instance || !cbs) continue
|
|
145
|
-
delete record.enteredCbs[name]
|
|
146
|
-
for (let i = 0; i < cbs.length; i++) {
|
|
147
|
-
if (!instance._isBeingDestroyed) cbs[i](instance)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import type KduRouter from '../index'
|
|
4
|
+
import { stringifyQuery } from './query'
|
|
5
|
+
|
|
6
|
+
const trailingSlashRE = /\/?$/
|
|
7
|
+
|
|
8
|
+
export function createRoute (
|
|
9
|
+
record: ?RouteRecord,
|
|
10
|
+
location: Location,
|
|
11
|
+
redirectedFrom?: ?Location,
|
|
12
|
+
router?: KduRouter
|
|
13
|
+
): Route {
|
|
14
|
+
const stringifyQuery = router && router.options.stringifyQuery
|
|
15
|
+
|
|
16
|
+
let query: any = location.query || {}
|
|
17
|
+
try {
|
|
18
|
+
query = clone(query)
|
|
19
|
+
} catch (e) {}
|
|
20
|
+
|
|
21
|
+
const route: Route = {
|
|
22
|
+
name: location.name || (record && record.name),
|
|
23
|
+
meta: (record && record.meta) || {},
|
|
24
|
+
path: location.path || '/',
|
|
25
|
+
hash: location.hash || '',
|
|
26
|
+
query,
|
|
27
|
+
params: location.params || {},
|
|
28
|
+
fullPath: getFullPath(location, stringifyQuery),
|
|
29
|
+
matched: record ? formatMatch(record) : []
|
|
30
|
+
}
|
|
31
|
+
if (redirectedFrom) {
|
|
32
|
+
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
|
|
33
|
+
}
|
|
34
|
+
return Object.freeze(route)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function clone (value) {
|
|
38
|
+
if (Array.isArray(value)) {
|
|
39
|
+
return value.map(clone)
|
|
40
|
+
} else if (value && typeof value === 'object') {
|
|
41
|
+
const res = {}
|
|
42
|
+
for (const key in value) {
|
|
43
|
+
res[key] = clone(value[key])
|
|
44
|
+
}
|
|
45
|
+
return res
|
|
46
|
+
} else {
|
|
47
|
+
return value
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// the starting route that represents the initial state
|
|
52
|
+
export const START = createRoute(null, {
|
|
53
|
+
path: '/'
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
|
|
57
|
+
const res = []
|
|
58
|
+
while (record) {
|
|
59
|
+
res.unshift(record)
|
|
60
|
+
record = record.parent
|
|
61
|
+
}
|
|
62
|
+
return res
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getFullPath (
|
|
66
|
+
{ path, query = {}, hash = '' },
|
|
67
|
+
_stringifyQuery
|
|
68
|
+
): string {
|
|
69
|
+
const stringify = _stringifyQuery || stringifyQuery
|
|
70
|
+
return (path || '/') + stringify(query) + hash
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function isSameRoute (a: Route, b: ?Route, onlyPath: ?boolean): boolean {
|
|
74
|
+
if (b === START) {
|
|
75
|
+
return a === b
|
|
76
|
+
} else if (!b) {
|
|
77
|
+
return false
|
|
78
|
+
} else if (a.path && b.path) {
|
|
79
|
+
return a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && (onlyPath ||
|
|
80
|
+
a.hash === b.hash &&
|
|
81
|
+
isObjectEqual(a.query, b.query))
|
|
82
|
+
} else if (a.name && b.name) {
|
|
83
|
+
return (
|
|
84
|
+
a.name === b.name &&
|
|
85
|
+
(onlyPath || (
|
|
86
|
+
a.hash === b.hash &&
|
|
87
|
+
isObjectEqual(a.query, b.query) &&
|
|
88
|
+
isObjectEqual(a.params, b.params))
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
} else {
|
|
92
|
+
return false
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isObjectEqual (a = {}, b = {}): boolean {
|
|
97
|
+
// handle null value #1566
|
|
98
|
+
if (!a || !b) return a === b
|
|
99
|
+
const aKeys = Object.keys(a).sort()
|
|
100
|
+
const bKeys = Object.keys(b).sort()
|
|
101
|
+
if (aKeys.length !== bKeys.length) {
|
|
102
|
+
return false
|
|
103
|
+
}
|
|
104
|
+
return aKeys.every((key, i) => {
|
|
105
|
+
const aVal = a[key]
|
|
106
|
+
const bKey = bKeys[i]
|
|
107
|
+
if (bKey !== key) return false
|
|
108
|
+
const bVal = b[key]
|
|
109
|
+
// query values can be null and undefined
|
|
110
|
+
if (aVal == null || bVal == null) return aVal === bVal
|
|
111
|
+
// check nested equality
|
|
112
|
+
if (typeof aVal === 'object' && typeof bVal === 'object') {
|
|
113
|
+
return isObjectEqual(aVal, bVal)
|
|
114
|
+
}
|
|
115
|
+
return String(aVal) === String(bVal)
|
|
116
|
+
})
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function isIncludedRoute (current: Route, target: Route): boolean {
|
|
120
|
+
return (
|
|
121
|
+
current.path.replace(trailingSlashRE, '/').indexOf(
|
|
122
|
+
target.path.replace(trailingSlashRE, '/')
|
|
123
|
+
) === 0 &&
|
|
124
|
+
(!target.hash || current.hash === target.hash) &&
|
|
125
|
+
queryIncludes(current.query, target.query)
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function queryIncludes (current: Dictionary<string>, target: Dictionary<string>): boolean {
|
|
130
|
+
for (const key in target) {
|
|
131
|
+
if (!(key in current)) {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return true
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function handleRouteEntered (route: Route) {
|
|
139
|
+
for (let i = 0; i < route.matched.length; i++) {
|
|
140
|
+
const record = route.matched[i]
|
|
141
|
+
for (const name in record.instances) {
|
|
142
|
+
const instance = record.instances[name]
|
|
143
|
+
const cbs = record.enteredCbs[name]
|
|
144
|
+
if (!instance || !cbs) continue
|
|
145
|
+
delete record.enteredCbs[name]
|
|
146
|
+
for (let i = 0; i < cbs.length; i++) {
|
|
147
|
+
if (!instance._isBeingDestroyed) cbs[i](instance)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
package/src/util/scroll.js
CHANGED
|
@@ -1,175 +1,175 @@
|
|
|
1
|
-
/* @flow */
|
|
2
|
-
|
|
3
|
-
import type Router from '../index'
|
|
4
|
-
import { assert } from './warn'
|
|
5
|
-
import { getStateKey, setStateKey } from './state-key'
|
|
6
|
-
import { extend } from './misc'
|
|
7
|
-
|
|
8
|
-
const positionStore = Object.create(null)
|
|
9
|
-
|
|
10
|
-
export function setupScroll () {
|
|
11
|
-
// Prevent browser scroll behavior on History popstate
|
|
12
|
-
if ('scrollRestoration' in window.history) {
|
|
13
|
-
window.history.scrollRestoration = 'manual'
|
|
14
|
-
}
|
|
15
|
-
// Fix for #1585 for Firefox
|
|
16
|
-
// Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
|
|
17
|
-
// Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
|
|
18
|
-
// window.location.protocol + '//' + window.location.host
|
|
19
|
-
// location.host contains the port and location.hostname doesn't
|
|
20
|
-
const protocolAndPath = window.location.protocol + '//' + window.location.host
|
|
21
|
-
const absolutePath = window.location.href.replace(protocolAndPath, '')
|
|
22
|
-
// preserve existing history state as it could be overriden by the user
|
|
23
|
-
const stateCopy = extend({}, window.history.state)
|
|
24
|
-
stateCopy.key = getStateKey()
|
|
25
|
-
window.history.replaceState(stateCopy, '', absolutePath)
|
|
26
|
-
window.addEventListener('popstate', handlePopState)
|
|
27
|
-
return () => {
|
|
28
|
-
window.removeEventListener('popstate', handlePopState)
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function handleScroll (
|
|
33
|
-
router: Router,
|
|
34
|
-
to: Route,
|
|
35
|
-
from: Route,
|
|
36
|
-
isPop: boolean
|
|
37
|
-
) {
|
|
38
|
-
if (!router.app) {
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const behavior = router.options.scrollBehavior
|
|
43
|
-
if (!behavior) {
|
|
44
|
-
return
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
48
|
-
assert(typeof behavior === 'function', `scrollBehavior must be a function`)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// wait until re-render finishes before scrolling
|
|
52
|
-
router.app.$nextTick(() => {
|
|
53
|
-
const position = getScrollPosition()
|
|
54
|
-
const shouldScroll = behavior.call(
|
|
55
|
-
router,
|
|
56
|
-
to,
|
|
57
|
-
from,
|
|
58
|
-
isPop ? position : null
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
if (!shouldScroll) {
|
|
62
|
-
return
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (typeof shouldScroll.then === 'function') {
|
|
66
|
-
shouldScroll
|
|
67
|
-
.then(shouldScroll => {
|
|
68
|
-
scrollToPosition((shouldScroll: any), position)
|
|
69
|
-
})
|
|
70
|
-
.catch(err => {
|
|
71
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
72
|
-
assert(false, err.toString())
|
|
73
|
-
}
|
|
74
|
-
})
|
|
75
|
-
} else {
|
|
76
|
-
scrollToPosition(shouldScroll, position)
|
|
77
|
-
}
|
|
78
|
-
})
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
export function saveScrollPosition () {
|
|
82
|
-
const key = getStateKey()
|
|
83
|
-
if (key) {
|
|
84
|
-
positionStore[key] = {
|
|
85
|
-
x: window.pageXOffset,
|
|
86
|
-
y: window.pageYOffset
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function handlePopState (e) {
|
|
92
|
-
saveScrollPosition()
|
|
93
|
-
if (e.state && e.state.key) {
|
|
94
|
-
setStateKey(e.state.key)
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function getScrollPosition (): ?Object {
|
|
99
|
-
const key = getStateKey()
|
|
100
|
-
if (key) {
|
|
101
|
-
return positionStore[key]
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getElementPosition (el: Element, offset: Object): Object {
|
|
106
|
-
const docEl: any = document.documentElement
|
|
107
|
-
const docRect = docEl.getBoundingClientRect()
|
|
108
|
-
const elRect = el.getBoundingClientRect()
|
|
109
|
-
return {
|
|
110
|
-
x: elRect.left - docRect.left - offset.x,
|
|
111
|
-
y: elRect.top - docRect.top - offset.y
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function isValidPosition (obj: Object): boolean {
|
|
116
|
-
return isNumber(obj.x) || isNumber(obj.y)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function normalizePosition (obj: Object): Object {
|
|
120
|
-
return {
|
|
121
|
-
x: isNumber(obj.x) ? obj.x : window.pageXOffset,
|
|
122
|
-
y: isNumber(obj.y) ? obj.y : window.pageYOffset
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function normalizeOffset (obj: Object): Object {
|
|
127
|
-
return {
|
|
128
|
-
x: isNumber(obj.x) ? obj.x : 0,
|
|
129
|
-
y: isNumber(obj.y) ? obj.y : 0
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function isNumber (v: any): boolean {
|
|
134
|
-
return typeof v === 'number'
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const hashStartsWithNumberRE = /^#\d/
|
|
138
|
-
|
|
139
|
-
function scrollToPosition (shouldScroll, position) {
|
|
140
|
-
const isObject = typeof shouldScroll === 'object'
|
|
141
|
-
if (isObject && typeof shouldScroll.selector === 'string') {
|
|
142
|
-
// getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
|
|
143
|
-
// but at the same time, it doesn't make much sense to select an element with an id and an extra selector
|
|
144
|
-
const el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line
|
|
145
|
-
? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line
|
|
146
|
-
: document.querySelector(shouldScroll.selector)
|
|
147
|
-
|
|
148
|
-
if (el) {
|
|
149
|
-
let offset =
|
|
150
|
-
shouldScroll.offset && typeof shouldScroll.offset === 'object'
|
|
151
|
-
? shouldScroll.offset
|
|
152
|
-
: {}
|
|
153
|
-
offset = normalizeOffset(offset)
|
|
154
|
-
position = getElementPosition(el, offset)
|
|
155
|
-
} else if (isValidPosition(shouldScroll)) {
|
|
156
|
-
position = normalizePosition(shouldScroll)
|
|
157
|
-
}
|
|
158
|
-
} else if (isObject && isValidPosition(shouldScroll)) {
|
|
159
|
-
position = normalizePosition(shouldScroll)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (position) {
|
|
163
|
-
// $flow-disable-line
|
|
164
|
-
if ('scrollBehavior' in document.documentElement.style) {
|
|
165
|
-
window.scrollTo({
|
|
166
|
-
left: position.x,
|
|
167
|
-
top: position.y,
|
|
168
|
-
// $flow-disable-line
|
|
169
|
-
behavior: shouldScroll.behavior
|
|
170
|
-
})
|
|
171
|
-
} else {
|
|
172
|
-
window.scrollTo(position.x, position.y)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import type Router from '../index'
|
|
4
|
+
import { assert } from './warn'
|
|
5
|
+
import { getStateKey, setStateKey } from './state-key'
|
|
6
|
+
import { extend } from './misc'
|
|
7
|
+
|
|
8
|
+
const positionStore = Object.create(null)
|
|
9
|
+
|
|
10
|
+
export function setupScroll () {
|
|
11
|
+
// Prevent browser scroll behavior on History popstate
|
|
12
|
+
if ('scrollRestoration' in window.history) {
|
|
13
|
+
window.history.scrollRestoration = 'manual'
|
|
14
|
+
}
|
|
15
|
+
// Fix for #1585 for Firefox
|
|
16
|
+
// Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
|
|
17
|
+
// Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
|
|
18
|
+
// window.location.protocol + '//' + window.location.host
|
|
19
|
+
// location.host contains the port and location.hostname doesn't
|
|
20
|
+
const protocolAndPath = window.location.protocol + '//' + window.location.host
|
|
21
|
+
const absolutePath = window.location.href.replace(protocolAndPath, '')
|
|
22
|
+
// preserve existing history state as it could be overriden by the user
|
|
23
|
+
const stateCopy = extend({}, window.history.state)
|
|
24
|
+
stateCopy.key = getStateKey()
|
|
25
|
+
window.history.replaceState(stateCopy, '', absolutePath)
|
|
26
|
+
window.addEventListener('popstate', handlePopState)
|
|
27
|
+
return () => {
|
|
28
|
+
window.removeEventListener('popstate', handlePopState)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function handleScroll (
|
|
33
|
+
router: Router,
|
|
34
|
+
to: Route,
|
|
35
|
+
from: Route,
|
|
36
|
+
isPop: boolean
|
|
37
|
+
) {
|
|
38
|
+
if (!router.app) {
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const behavior = router.options.scrollBehavior
|
|
43
|
+
if (!behavior) {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
48
|
+
assert(typeof behavior === 'function', `scrollBehavior must be a function`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// wait until re-render finishes before scrolling
|
|
52
|
+
router.app.$nextTick(() => {
|
|
53
|
+
const position = getScrollPosition()
|
|
54
|
+
const shouldScroll = behavior.call(
|
|
55
|
+
router,
|
|
56
|
+
to,
|
|
57
|
+
from,
|
|
58
|
+
isPop ? position : null
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if (!shouldScroll) {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (typeof shouldScroll.then === 'function') {
|
|
66
|
+
shouldScroll
|
|
67
|
+
.then(shouldScroll => {
|
|
68
|
+
scrollToPosition((shouldScroll: any), position)
|
|
69
|
+
})
|
|
70
|
+
.catch(err => {
|
|
71
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
72
|
+
assert(false, err.toString())
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
} else {
|
|
76
|
+
scrollToPosition(shouldScroll, position)
|
|
77
|
+
}
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function saveScrollPosition () {
|
|
82
|
+
const key = getStateKey()
|
|
83
|
+
if (key) {
|
|
84
|
+
positionStore[key] = {
|
|
85
|
+
x: window.pageXOffset,
|
|
86
|
+
y: window.pageYOffset
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function handlePopState (e) {
|
|
92
|
+
saveScrollPosition()
|
|
93
|
+
if (e.state && e.state.key) {
|
|
94
|
+
setStateKey(e.state.key)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function getScrollPosition (): ?Object {
|
|
99
|
+
const key = getStateKey()
|
|
100
|
+
if (key) {
|
|
101
|
+
return positionStore[key]
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getElementPosition (el: Element, offset: Object): Object {
|
|
106
|
+
const docEl: any = document.documentElement
|
|
107
|
+
const docRect = docEl.getBoundingClientRect()
|
|
108
|
+
const elRect = el.getBoundingClientRect()
|
|
109
|
+
return {
|
|
110
|
+
x: elRect.left - docRect.left - offset.x,
|
|
111
|
+
y: elRect.top - docRect.top - offset.y
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isValidPosition (obj: Object): boolean {
|
|
116
|
+
return isNumber(obj.x) || isNumber(obj.y)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function normalizePosition (obj: Object): Object {
|
|
120
|
+
return {
|
|
121
|
+
x: isNumber(obj.x) ? obj.x : window.pageXOffset,
|
|
122
|
+
y: isNumber(obj.y) ? obj.y : window.pageYOffset
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeOffset (obj: Object): Object {
|
|
127
|
+
return {
|
|
128
|
+
x: isNumber(obj.x) ? obj.x : 0,
|
|
129
|
+
y: isNumber(obj.y) ? obj.y : 0
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isNumber (v: any): boolean {
|
|
134
|
+
return typeof v === 'number'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hashStartsWithNumberRE = /^#\d/
|
|
138
|
+
|
|
139
|
+
function scrollToPosition (shouldScroll, position) {
|
|
140
|
+
const isObject = typeof shouldScroll === 'object'
|
|
141
|
+
if (isObject && typeof shouldScroll.selector === 'string') {
|
|
142
|
+
// getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
|
|
143
|
+
// but at the same time, it doesn't make much sense to select an element with an id and an extra selector
|
|
144
|
+
const el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line
|
|
145
|
+
? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line
|
|
146
|
+
: document.querySelector(shouldScroll.selector)
|
|
147
|
+
|
|
148
|
+
if (el) {
|
|
149
|
+
let offset =
|
|
150
|
+
shouldScroll.offset && typeof shouldScroll.offset === 'object'
|
|
151
|
+
? shouldScroll.offset
|
|
152
|
+
: {}
|
|
153
|
+
offset = normalizeOffset(offset)
|
|
154
|
+
position = getElementPosition(el, offset)
|
|
155
|
+
} else if (isValidPosition(shouldScroll)) {
|
|
156
|
+
position = normalizePosition(shouldScroll)
|
|
157
|
+
}
|
|
158
|
+
} else if (isObject && isValidPosition(shouldScroll)) {
|
|
159
|
+
position = normalizePosition(shouldScroll)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (position) {
|
|
163
|
+
// $flow-disable-line
|
|
164
|
+
if ('scrollBehavior' in document.documentElement.style) {
|
|
165
|
+
window.scrollTo({
|
|
166
|
+
left: position.x,
|
|
167
|
+
top: position.y,
|
|
168
|
+
// $flow-disable-line
|
|
169
|
+
behavior: shouldScroll.behavior
|
|
170
|
+
})
|
|
171
|
+
} else {
|
|
172
|
+
window.scrollTo(position.x, position.y)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|