kdu-router 3.0.7 → 3.1.7
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/dist/kdu-router.common.js +729 -486
- package/dist/kdu-router.esm.browser.js +712 -477
- package/dist/kdu-router.esm.browser.min.js +3 -3
- package/dist/kdu-router.esm.js +729 -486
- package/dist/kdu-router.js +2591 -2348
- package/dist/kdu-router.min.js +3 -3
- package/package.json +8 -8
- package/src/components/link.js +190 -0
- package/src/components/view.js +149 -0
- package/src/create-route-map.js +69 -35
- package/src/history/abstract.js +69 -0
- package/src/history/base.js +352 -0
- package/src/history/errors.js +22 -0
- package/src/history/hash.js +154 -0
- package/src/history/html5.js +80 -0
- package/src/index.js +16 -2
- package/src/util/async.js +18 -0
- package/src/util/dom.js +3 -0
- package/src/util/location.js +69 -0
- package/src/util/misc.js +6 -0
- package/src/util/params.js +37 -0
- package/src/util/path.js +74 -0
- package/src/util/push-state.js +46 -0
- package/src/util/query.js +95 -0
- package/src/util/resolve-components.js +108 -0
- package/src/util/route.js +132 -0
- package/src/util/scroll.js +156 -0
- package/src/util/state-key.js +22 -0
- package/src/util/warn.js +25 -0
- package/types/index.d.ts +4 -4
- package/types/kdu.d.ts +10 -10
- package/types/router.d.ts +104 -86
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import type Router from '../index'
|
|
4
|
+
import { History } from './base'
|
|
5
|
+
import { cleanPath } from '../util/path'
|
|
6
|
+
import { getLocation } from './html5'
|
|
7
|
+
import { setupScroll, handleScroll } from '../util/scroll'
|
|
8
|
+
import { pushState, replaceState, supportsPushState } from '../util/push-state'
|
|
9
|
+
|
|
10
|
+
export class HashHistory extends History {
|
|
11
|
+
constructor (router: Router, base: ?string, fallback: boolean) {
|
|
12
|
+
super(router, base)
|
|
13
|
+
// check history fallback deeplinking
|
|
14
|
+
if (fallback && checkFallback(this.base)) {
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
ensureSlash()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// this is delayed until the app mounts
|
|
21
|
+
// to avoid the hashchange listener being fired too early
|
|
22
|
+
setupListeners () {
|
|
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(
|
|
32
|
+
supportsPushState ? 'popstate' : 'hashchange',
|
|
33
|
+
() => {
|
|
34
|
+
const current = this.current
|
|
35
|
+
if (!ensureSlash()) {
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
this.transitionTo(getHash(), route => {
|
|
39
|
+
if (supportsScroll) {
|
|
40
|
+
handleScroll(this.router, route, current, true)
|
|
41
|
+
}
|
|
42
|
+
if (!supportsPushState) {
|
|
43
|
+
replaceHash(route.fullPath)
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
|
|
51
|
+
const { current: fromRoute } = this
|
|
52
|
+
this.transitionTo(
|
|
53
|
+
location,
|
|
54
|
+
route => {
|
|
55
|
+
pushHash(route.fullPath)
|
|
56
|
+
handleScroll(this.router, route, fromRoute, false)
|
|
57
|
+
onComplete && onComplete(route)
|
|
58
|
+
},
|
|
59
|
+
onAbort
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
|
|
64
|
+
const { current: fromRoute } = this
|
|
65
|
+
this.transitionTo(
|
|
66
|
+
location,
|
|
67
|
+
route => {
|
|
68
|
+
replaceHash(route.fullPath)
|
|
69
|
+
handleScroll(this.router, route, fromRoute, false)
|
|
70
|
+
onComplete && onComplete(route)
|
|
71
|
+
},
|
|
72
|
+
onAbort
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
go (n: number) {
|
|
77
|
+
window.history.go(n)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
ensureURL (push?: boolean) {
|
|
81
|
+
const current = this.current.fullPath
|
|
82
|
+
if (getHash() !== current) {
|
|
83
|
+
push ? pushHash(current) : replaceHash(current)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
getCurrentLocation () {
|
|
88
|
+
return getHash()
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function checkFallback (base) {
|
|
93
|
+
const location = getLocation(base)
|
|
94
|
+
if (!/^\/#/.test(location)) {
|
|
95
|
+
window.location.replace(cleanPath(base + '/#' + location))
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function ensureSlash (): boolean {
|
|
101
|
+
const path = getHash()
|
|
102
|
+
if (path.charAt(0) === '/') {
|
|
103
|
+
return true
|
|
104
|
+
}
|
|
105
|
+
replaceHash('/' + path)
|
|
106
|
+
return false
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function getHash (): string {
|
|
110
|
+
// We can't use window.location.hash here because it's not
|
|
111
|
+
// consistent across browsers - Firefox will pre-decode it!
|
|
112
|
+
let href = window.location.href
|
|
113
|
+
const index = href.indexOf('#')
|
|
114
|
+
// empty path
|
|
115
|
+
if (index < 0) return ''
|
|
116
|
+
|
|
117
|
+
href = href.slice(index + 1)
|
|
118
|
+
// decode the hash but not the search or hash
|
|
119
|
+
// as search(query) is already decoded
|
|
120
|
+
const searchIndex = href.indexOf('?')
|
|
121
|
+
if (searchIndex < 0) {
|
|
122
|
+
const hashIndex = href.indexOf('#')
|
|
123
|
+
if (hashIndex > -1) {
|
|
124
|
+
href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
|
|
125
|
+
} else href = decodeURI(href)
|
|
126
|
+
} else {
|
|
127
|
+
href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return href
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function getUrl (path) {
|
|
134
|
+
const href = window.location.href
|
|
135
|
+
const i = href.indexOf('#')
|
|
136
|
+
const base = i >= 0 ? href.slice(0, i) : href
|
|
137
|
+
return `${base}#${path}`
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function pushHash (path) {
|
|
141
|
+
if (supportsPushState) {
|
|
142
|
+
pushState(getUrl(path))
|
|
143
|
+
} else {
|
|
144
|
+
window.location.hash = path
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function replaceHash (path) {
|
|
149
|
+
if (supportsPushState) {
|
|
150
|
+
replaceState(getUrl(path))
|
|
151
|
+
} else {
|
|
152
|
+
window.location.replace(getUrl(path))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import type Router from '../index'
|
|
4
|
+
import { History } from './base'
|
|
5
|
+
import { cleanPath } from '../util/path'
|
|
6
|
+
import { START } from '../util/route'
|
|
7
|
+
import { setupScroll, handleScroll } from '../util/scroll'
|
|
8
|
+
import { pushState, replaceState, supportsPushState } from '../util/push-state'
|
|
9
|
+
|
|
10
|
+
export class HTML5History extends History {
|
|
11
|
+
constructor (router: Router, base: ?string) {
|
|
12
|
+
super(router, base)
|
|
13
|
+
|
|
14
|
+
const expectScroll = router.options.scrollBehavior
|
|
15
|
+
const supportsScroll = supportsPushState && expectScroll
|
|
16
|
+
|
|
17
|
+
if (supportsScroll) {
|
|
18
|
+
setupScroll()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const initLocation = getLocation(this.base)
|
|
22
|
+
window.addEventListener('popstate', e => {
|
|
23
|
+
const current = this.current
|
|
24
|
+
|
|
25
|
+
// Avoiding first `popstate` event dispatched in some browsers but first
|
|
26
|
+
// history route not updated since async guard at the same time.
|
|
27
|
+
const location = getLocation(this.base)
|
|
28
|
+
if (this.current === START && location === initLocation) {
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
this.transitionTo(location, route => {
|
|
33
|
+
if (supportsScroll) {
|
|
34
|
+
handleScroll(router, route, current, true)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
go (n: number) {
|
|
41
|
+
window.history.go(n)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
|
|
45
|
+
const { current: fromRoute } = this
|
|
46
|
+
this.transitionTo(location, route => {
|
|
47
|
+
pushState(cleanPath(this.base + route.fullPath))
|
|
48
|
+
handleScroll(this.router, route, fromRoute, false)
|
|
49
|
+
onComplete && onComplete(route)
|
|
50
|
+
}, onAbort)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
|
|
54
|
+
const { current: fromRoute } = this
|
|
55
|
+
this.transitionTo(location, route => {
|
|
56
|
+
replaceState(cleanPath(this.base + route.fullPath))
|
|
57
|
+
handleScroll(this.router, route, fromRoute, false)
|
|
58
|
+
onComplete && onComplete(route)
|
|
59
|
+
}, onAbort)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ensureURL (push?: boolean) {
|
|
63
|
+
if (getLocation(this.base) !== this.current.fullPath) {
|
|
64
|
+
const current = cleanPath(this.base + this.current.fullPath)
|
|
65
|
+
push ? pushState(current) : replaceState(current)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
getCurrentLocation (): string {
|
|
70
|
+
return getLocation(this.base)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getLocation (base: string): string {
|
|
75
|
+
let path = decodeURI(window.location.pathname)
|
|
76
|
+
if (base && path.indexOf(base) === 0) {
|
|
77
|
+
path = path.slice(base.length)
|
|
78
|
+
}
|
|
79
|
+
return (path || '/') + window.location.search + window.location.hash
|
|
80
|
+
}
|
package/src/index.js
CHANGED
|
@@ -150,11 +150,25 @@ export default class KduRouter {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
|
|
153
|
-
|
|
153
|
+
// $flow-disable-line
|
|
154
|
+
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
this.history.push(location, resolve, reject)
|
|
157
|
+
})
|
|
158
|
+
} else {
|
|
159
|
+
this.history.push(location, onComplete, onAbort)
|
|
160
|
+
}
|
|
154
161
|
}
|
|
155
162
|
|
|
156
163
|
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
|
|
157
|
-
|
|
164
|
+
// $flow-disable-line
|
|
165
|
+
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
|
|
166
|
+
return new Promise((resolve, reject) => {
|
|
167
|
+
this.history.replace(location, resolve, reject)
|
|
168
|
+
})
|
|
169
|
+
} else {
|
|
170
|
+
this.history.replace(location, onComplete, onAbort)
|
|
171
|
+
}
|
|
158
172
|
}
|
|
159
173
|
|
|
160
174
|
go (n: number) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
|
|
4
|
+
const step = index => {
|
|
5
|
+
if (index >= queue.length) {
|
|
6
|
+
cb()
|
|
7
|
+
} else {
|
|
8
|
+
if (queue[index]) {
|
|
9
|
+
fn(queue[index], () => {
|
|
10
|
+
step(index + 1)
|
|
11
|
+
})
|
|
12
|
+
} else {
|
|
13
|
+
step(index + 1)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
step(0)
|
|
18
|
+
}
|
package/src/util/dom.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import type KduRouter from '../index'
|
|
4
|
+
import { parsePath, resolvePath } from './path'
|
|
5
|
+
import { resolveQuery } from './query'
|
|
6
|
+
import { fillParams } from './params'
|
|
7
|
+
import { warn } from './warn'
|
|
8
|
+
import { extend } from './misc'
|
|
9
|
+
|
|
10
|
+
export function normalizeLocation (
|
|
11
|
+
raw: RawLocation,
|
|
12
|
+
current: ?Route,
|
|
13
|
+
append: ?boolean,
|
|
14
|
+
router: ?KduRouter
|
|
15
|
+
): Location {
|
|
16
|
+
let next: Location = typeof raw === 'string' ? { path: raw } : raw
|
|
17
|
+
// named target
|
|
18
|
+
if (next._normalized) {
|
|
19
|
+
return next
|
|
20
|
+
} else if (next.name) {
|
|
21
|
+
next = extend({}, raw)
|
|
22
|
+
const params = next.params
|
|
23
|
+
if (params && typeof params === 'object') {
|
|
24
|
+
next.params = extend({}, params)
|
|
25
|
+
}
|
|
26
|
+
return next
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// relative params
|
|
30
|
+
if (!next.path && next.params && current) {
|
|
31
|
+
next = extend({}, next)
|
|
32
|
+
next._normalized = true
|
|
33
|
+
const params: any = extend(extend({}, current.params), next.params)
|
|
34
|
+
if (current.name) {
|
|
35
|
+
next.name = current.name
|
|
36
|
+
next.params = params
|
|
37
|
+
} else if (current.matched.length) {
|
|
38
|
+
const rawPath = current.matched[current.matched.length - 1].path
|
|
39
|
+
next.path = fillParams(rawPath, params, `path ${current.path}`)
|
|
40
|
+
} else if (process.env.NODE_ENV !== 'production') {
|
|
41
|
+
warn(false, `relative params navigation requires a current route.`)
|
|
42
|
+
}
|
|
43
|
+
return next
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const parsedPath = parsePath(next.path || '')
|
|
47
|
+
const basePath = (current && current.path) || '/'
|
|
48
|
+
const path = parsedPath.path
|
|
49
|
+
? resolvePath(parsedPath.path, basePath, append || next.append)
|
|
50
|
+
: basePath
|
|
51
|
+
|
|
52
|
+
const query = resolveQuery(
|
|
53
|
+
parsedPath.query,
|
|
54
|
+
next.query,
|
|
55
|
+
router && router.options.parseQuery
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
let hash = next.hash || parsedPath.hash
|
|
59
|
+
if (hash && hash.charAt(0) !== '#') {
|
|
60
|
+
hash = `#${hash}`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
_normalized: true,
|
|
65
|
+
path,
|
|
66
|
+
query,
|
|
67
|
+
hash
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/util/misc.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import { warn } from './warn'
|
|
4
|
+
import Regexp from 'path-to-regexp'
|
|
5
|
+
|
|
6
|
+
// $flow-disable-line
|
|
7
|
+
const regexpCompileCache: {
|
|
8
|
+
[key: string]: Function
|
|
9
|
+
} = Object.create(null)
|
|
10
|
+
|
|
11
|
+
export function fillParams (
|
|
12
|
+
path: string,
|
|
13
|
+
params: ?Object,
|
|
14
|
+
routeMsg: string
|
|
15
|
+
): string {
|
|
16
|
+
params = params || {}
|
|
17
|
+
try {
|
|
18
|
+
const filler =
|
|
19
|
+
regexpCompileCache[path] ||
|
|
20
|
+
(regexpCompileCache[path] = Regexp.compile(path))
|
|
21
|
+
|
|
22
|
+
// Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }}
|
|
23
|
+
// and fix #3106 so that you can work with location descriptor object having params.pathMatch equal to empty string
|
|
24
|
+
if (typeof params.pathMatch === 'string') params[0] = params.pathMatch
|
|
25
|
+
|
|
26
|
+
return filler(params, { pretty: true })
|
|
27
|
+
} catch (e) {
|
|
28
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
29
|
+
// Fix #3072 no warn if `pathMatch` is string
|
|
30
|
+
warn(typeof params.pathMatch === 'string', `missing param for ${routeMsg}: ${e.message}`)
|
|
31
|
+
}
|
|
32
|
+
return ''
|
|
33
|
+
} finally {
|
|
34
|
+
// delete the 0 if it was added
|
|
35
|
+
delete params[0]
|
|
36
|
+
}
|
|
37
|
+
}
|
package/src/util/path.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
export function resolvePath (
|
|
4
|
+
relative: string,
|
|
5
|
+
base: string,
|
|
6
|
+
append?: boolean
|
|
7
|
+
): string {
|
|
8
|
+
const firstChar = relative.charAt(0)
|
|
9
|
+
if (firstChar === '/') {
|
|
10
|
+
return relative
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
if (firstChar === '?' || firstChar === '#') {
|
|
14
|
+
return base + relative
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const stack = base.split('/')
|
|
18
|
+
|
|
19
|
+
// remove trailing segment if:
|
|
20
|
+
// - not appending
|
|
21
|
+
// - appending to trailing slash (last segment is empty)
|
|
22
|
+
if (!append || !stack[stack.length - 1]) {
|
|
23
|
+
stack.pop()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// resolve relative path
|
|
27
|
+
const segments = relative.replace(/^\//, '').split('/')
|
|
28
|
+
for (let i = 0; i < segments.length; i++) {
|
|
29
|
+
const segment = segments[i]
|
|
30
|
+
if (segment === '..') {
|
|
31
|
+
stack.pop()
|
|
32
|
+
} else if (segment !== '.') {
|
|
33
|
+
stack.push(segment)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ensure leading slash
|
|
38
|
+
if (stack[0] !== '') {
|
|
39
|
+
stack.unshift('')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return stack.join('/')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function parsePath (path: string): {
|
|
46
|
+
path: string;
|
|
47
|
+
query: string;
|
|
48
|
+
hash: string;
|
|
49
|
+
} {
|
|
50
|
+
let hash = ''
|
|
51
|
+
let query = ''
|
|
52
|
+
|
|
53
|
+
const hashIndex = path.indexOf('#')
|
|
54
|
+
if (hashIndex >= 0) {
|
|
55
|
+
hash = path.slice(hashIndex)
|
|
56
|
+
path = path.slice(0, hashIndex)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const queryIndex = path.indexOf('?')
|
|
60
|
+
if (queryIndex >= 0) {
|
|
61
|
+
query = path.slice(queryIndex + 1)
|
|
62
|
+
path = path.slice(0, queryIndex)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
path,
|
|
67
|
+
query,
|
|
68
|
+
hash
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function cleanPath (path: string): string {
|
|
73
|
+
return path.replace(/\/\//g, '/')
|
|
74
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import { inBrowser } from './dom'
|
|
4
|
+
import { saveScrollPosition } from './scroll'
|
|
5
|
+
import { genStateKey, setStateKey, getStateKey } from './state-key'
|
|
6
|
+
import { extend } from './misc'
|
|
7
|
+
|
|
8
|
+
export const supportsPushState =
|
|
9
|
+
inBrowser &&
|
|
10
|
+
(function () {
|
|
11
|
+
const ua = window.navigator.userAgent
|
|
12
|
+
|
|
13
|
+
if (
|
|
14
|
+
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
|
|
15
|
+
ua.indexOf('Mobile Safari') !== -1 &&
|
|
16
|
+
ua.indexOf('Chrome') === -1 &&
|
|
17
|
+
ua.indexOf('Windows Phone') === -1
|
|
18
|
+
) {
|
|
19
|
+
return false
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return window.history && 'pushState' in window.history
|
|
23
|
+
})()
|
|
24
|
+
|
|
25
|
+
export function pushState (url?: string, replace?: boolean) {
|
|
26
|
+
saveScrollPosition()
|
|
27
|
+
// try...catch the pushState call to get around Safari
|
|
28
|
+
// DOM Exception 18 where it limits to 100 pushState calls
|
|
29
|
+
const history = window.history
|
|
30
|
+
try {
|
|
31
|
+
if (replace) {
|
|
32
|
+
// preserve existing history state as it could be overriden by the user
|
|
33
|
+
const stateCopy = extend({}, history.state)
|
|
34
|
+
stateCopy.key = getStateKey()
|
|
35
|
+
history.replaceState(stateCopy, '', url)
|
|
36
|
+
} else {
|
|
37
|
+
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
|
|
38
|
+
}
|
|
39
|
+
} catch (e) {
|
|
40
|
+
window.location[replace ? 'replace' : 'assign'](url)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function replaceState (url?: string) {
|
|
45
|
+
pushState(url, true)
|
|
46
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/* @flow */
|
|
2
|
+
|
|
3
|
+
import { warn } from './warn'
|
|
4
|
+
|
|
5
|
+
const encodeReserveRE = /[!'()*]/g
|
|
6
|
+
const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16)
|
|
7
|
+
const commaRE = /%2C/g
|
|
8
|
+
|
|
9
|
+
// fixed encodeURIComponent which is more conformant to RFC3986:
|
|
10
|
+
// - escapes [!'()*]
|
|
11
|
+
// - preserve commas
|
|
12
|
+
const encode = str => encodeURIComponent(str)
|
|
13
|
+
.replace(encodeReserveRE, encodeReserveReplacer)
|
|
14
|
+
.replace(commaRE, ',')
|
|
15
|
+
|
|
16
|
+
const decode = decodeURIComponent
|
|
17
|
+
|
|
18
|
+
export function resolveQuery (
|
|
19
|
+
query: ?string,
|
|
20
|
+
extraQuery: Dictionary<string> = {},
|
|
21
|
+
_parseQuery: ?Function
|
|
22
|
+
): Dictionary<string> {
|
|
23
|
+
const parse = _parseQuery || parseQuery
|
|
24
|
+
let parsedQuery
|
|
25
|
+
try {
|
|
26
|
+
parsedQuery = parse(query || '')
|
|
27
|
+
} catch (e) {
|
|
28
|
+
process.env.NODE_ENV !== 'production' && warn(false, e.message)
|
|
29
|
+
parsedQuery = {}
|
|
30
|
+
}
|
|
31
|
+
for (const key in extraQuery) {
|
|
32
|
+
parsedQuery[key] = extraQuery[key]
|
|
33
|
+
}
|
|
34
|
+
return parsedQuery
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseQuery (query: string): Dictionary<string> {
|
|
38
|
+
const res = {}
|
|
39
|
+
|
|
40
|
+
query = query.trim().replace(/^(\?|#|&)/, '')
|
|
41
|
+
|
|
42
|
+
if (!query) {
|
|
43
|
+
return res
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
query.split('&').forEach(param => {
|
|
47
|
+
const parts = param.replace(/\+/g, ' ').split('=')
|
|
48
|
+
const key = decode(parts.shift())
|
|
49
|
+
const val = parts.length > 0
|
|
50
|
+
? decode(parts.join('='))
|
|
51
|
+
: null
|
|
52
|
+
|
|
53
|
+
if (res[key] === undefined) {
|
|
54
|
+
res[key] = val
|
|
55
|
+
} else if (Array.isArray(res[key])) {
|
|
56
|
+
res[key].push(val)
|
|
57
|
+
} else {
|
|
58
|
+
res[key] = [res[key], val]
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return res
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function stringifyQuery (obj: Dictionary<string>): string {
|
|
66
|
+
const res = obj ? Object.keys(obj).map(key => {
|
|
67
|
+
const val = obj[key]
|
|
68
|
+
|
|
69
|
+
if (val === undefined) {
|
|
70
|
+
return ''
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (val === null) {
|
|
74
|
+
return encode(key)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (Array.isArray(val)) {
|
|
78
|
+
const result = []
|
|
79
|
+
val.forEach(val2 => {
|
|
80
|
+
if (val2 === undefined) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
if (val2 === null) {
|
|
84
|
+
result.push(encode(key))
|
|
85
|
+
} else {
|
|
86
|
+
result.push(encode(key) + '=' + encode(val2))
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
return result.join('&')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return encode(key) + '=' + encode(val)
|
|
93
|
+
}).filter(x => x.length > 0).join('&') : null
|
|
94
|
+
return res ? `?${res}` : ''
|
|
95
|
+
}
|