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.
@@ -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
- this.history.push(location, onComplete, onAbort)
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
- this.history.replace(location, onComplete, onAbort)
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
+ }
@@ -0,0 +1,3 @@
1
+ /* @flow */
2
+
3
+ export const inBrowser = typeof window !== 'undefined'
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export function extend (a, b) {
2
+ for (const key in b) {
3
+ a[key] = b[key]
4
+ }
5
+ return a
6
+ }
@@ -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
+ }
@@ -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
+ }