kdu-router 2.7.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/src/index.js ADDED
@@ -0,0 +1,235 @@
1
+ /* @flow */
2
+
3
+ import { install } from './install'
4
+ import { START } from './util/route'
5
+ import { assert } from './util/warn'
6
+ import { inBrowser } from './util/dom'
7
+ import { cleanPath } from './util/path'
8
+ import { createMatcher } from './create-matcher'
9
+ import { normalizeLocation } from './util/location'
10
+ import { supportsPushState } from './util/push-state'
11
+
12
+ import { HashHistory } from './history/hash'
13
+ import { HTML5History } from './history/html5'
14
+ import { AbstractHistory } from './history/abstract'
15
+
16
+ import type { Matcher } from './create-matcher'
17
+
18
+ export default class KduRouter {
19
+ static install: () => void;
20
+ static version: string;
21
+
22
+ app: any;
23
+ apps: Array<any>;
24
+ ready: boolean;
25
+ readyCbs: Array<Function>;
26
+ options: RouterOptions;
27
+ mode: string;
28
+ history: HashHistory | HTML5History | AbstractHistory;
29
+ matcher: Matcher;
30
+ fallback: boolean;
31
+ beforeHooks: Array<?NavigationGuard>;
32
+ resolveHooks: Array<?NavigationGuard>;
33
+ afterHooks: Array<?AfterNavigationHook>;
34
+
35
+ constructor (options: RouterOptions = {}) {
36
+ this.app = null
37
+ this.apps = []
38
+ this.options = options
39
+ this.beforeHooks = []
40
+ this.resolveHooks = []
41
+ this.afterHooks = []
42
+ this.matcher = createMatcher(options.routes || [], this)
43
+
44
+ let mode = options.mode || 'hash'
45
+ this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
46
+ if (this.fallback) {
47
+ mode = 'hash'
48
+ }
49
+ if (!inBrowser) {
50
+ mode = 'abstract'
51
+ }
52
+ this.mode = mode
53
+
54
+ switch (mode) {
55
+ case 'history':
56
+ this.history = new HTML5History(this, options.base)
57
+ break
58
+ case 'hash':
59
+ this.history = new HashHistory(this, options.base, this.fallback)
60
+ break
61
+ case 'abstract':
62
+ this.history = new AbstractHistory(this, options.base)
63
+ break
64
+ default:
65
+ if (process.env.NODE_ENV !== 'production') {
66
+ assert(false, `invalid mode: ${mode}`)
67
+ }
68
+ }
69
+ }
70
+
71
+ match (
72
+ raw: RawLocation,
73
+ current?: Route,
74
+ redirectedFrom?: Location
75
+ ): Route {
76
+ return this.matcher.match(raw, current, redirectedFrom)
77
+ }
78
+
79
+ get currentRoute (): ?Route {
80
+ return this.history && this.history.current
81
+ }
82
+
83
+ init (app: any /* Kdu component instance */) {
84
+ process.env.NODE_ENV !== 'production' && assert(
85
+ install.installed,
86
+ `not installed. Make sure to call \`Kdu.use(KduRouter)\` ` +
87
+ `before creating root instance.`
88
+ )
89
+
90
+ this.apps.push(app)
91
+
92
+ // main app already initialized.
93
+ if (this.app) {
94
+ return
95
+ }
96
+
97
+ this.app = app
98
+
99
+ const history = this.history
100
+
101
+ if (history instanceof HTML5History) {
102
+ history.transitionTo(history.getCurrentLocation())
103
+ } else if (history instanceof HashHistory) {
104
+ const setupHashListener = () => {
105
+ history.setupListeners()
106
+ }
107
+ history.transitionTo(
108
+ history.getCurrentLocation(),
109
+ setupHashListener,
110
+ setupHashListener
111
+ )
112
+ }
113
+
114
+ history.listen(route => {
115
+ this.apps.forEach((app) => {
116
+ app._route = route
117
+ })
118
+ })
119
+ }
120
+
121
+ beforeEach (fn: Function): Function {
122
+ return registerHook(this.beforeHooks, fn)
123
+ }
124
+
125
+ beforeResolve (fn: Function): Function {
126
+ return registerHook(this.resolveHooks, fn)
127
+ }
128
+
129
+ afterEach (fn: Function): Function {
130
+ return registerHook(this.afterHooks, fn)
131
+ }
132
+
133
+ onReady (cb: Function, errorCb?: Function) {
134
+ this.history.onReady(cb, errorCb)
135
+ }
136
+
137
+ onError (errorCb: Function) {
138
+ this.history.onError(errorCb)
139
+ }
140
+
141
+ push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
142
+ this.history.push(location, onComplete, onAbort)
143
+ }
144
+
145
+ replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
146
+ this.history.replace(location, onComplete, onAbort)
147
+ }
148
+
149
+ go (n: number) {
150
+ this.history.go(n)
151
+ }
152
+
153
+ back () {
154
+ this.go(-1)
155
+ }
156
+
157
+ forward () {
158
+ this.go(1)
159
+ }
160
+
161
+ getMatchedComponents (to?: RawLocation | Route): Array<any> {
162
+ const route: any = to
163
+ ? to.matched
164
+ ? to
165
+ : this.resolve(to).route
166
+ : this.currentRoute
167
+ if (!route) {
168
+ return []
169
+ }
170
+ return [].concat.apply([], route.matched.map(m => {
171
+ return Object.keys(m.components).map(key => {
172
+ return m.components[key]
173
+ })
174
+ }))
175
+ }
176
+
177
+ resolve (
178
+ to: RawLocation,
179
+ current?: Route,
180
+ append?: boolean
181
+ ): {
182
+ location: Location,
183
+ route: Route,
184
+ href: string,
185
+ // for backwards compat
186
+ normalizedTo: Location,
187
+ resolved: Route
188
+ } {
189
+ const location = normalizeLocation(
190
+ to,
191
+ current || this.history.current,
192
+ append,
193
+ this
194
+ )
195
+ const route = this.match(location, current)
196
+ const fullPath = route.redirectedFrom || route.fullPath
197
+ const base = this.history.base
198
+ const href = createHref(base, fullPath, this.mode)
199
+ return {
200
+ location,
201
+ route,
202
+ href,
203
+ // for backwards compat
204
+ normalizedTo: location,
205
+ resolved: route
206
+ }
207
+ }
208
+
209
+ addRoutes (routes: Array<RouteConfig>) {
210
+ this.matcher.addRoutes(routes)
211
+ if (this.history.current !== START) {
212
+ this.history.transitionTo(this.history.getCurrentLocation())
213
+ }
214
+ }
215
+ }
216
+
217
+ function registerHook (list: Array<any>, fn: Function): Function {
218
+ list.push(fn)
219
+ return () => {
220
+ const i = list.indexOf(fn)
221
+ if (i > -1) list.splice(i, 1)
222
+ }
223
+ }
224
+
225
+ function createHref (base: string, fullPath: string, mode) {
226
+ var path = mode === 'hash' ? '#' + fullPath : fullPath
227
+ return base ? cleanPath(base + '/' + path) : path
228
+ }
229
+
230
+ KduRouter.install = install
231
+ KduRouter.version = '__VERSION__'
232
+
233
+ if (inBrowser && window.Kdu) {
234
+ window.Kdu.use(KduRouter)
235
+ }
package/src/install.js ADDED
@@ -0,0 +1,52 @@
1
+ import View from './components/view'
2
+ import Link from './components/link'
3
+
4
+ export let _Kdu
5
+
6
+ export function install (Kdu) {
7
+ if (install.installed) return
8
+ install.installed = true
9
+
10
+ _Kdu = Kdu
11
+
12
+ const isDef = v => v !== undefined
13
+
14
+ const registerInstance = (vm, callVal) => {
15
+ let i = vm.$options._parentVnode
16
+ if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
17
+ i(vm, callVal)
18
+ }
19
+ }
20
+
21
+ Kdu.mixin({
22
+ beforeCreate () {
23
+ if (isDef(this.$options.router)) {
24
+ this._routerRoot = this
25
+ this._router = this.$options.router
26
+ this._router.init(this)
27
+ Kdu.util.defineReactive(this, '_route', this._router.history.current)
28
+ } else {
29
+ this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
30
+ }
31
+ registerInstance(this, this)
32
+ },
33
+ destroyed () {
34
+ registerInstance(this)
35
+ }
36
+ })
37
+
38
+ Object.defineProperty(Kdu.prototype, '$router', {
39
+ get () { return this._routerRoot._router }
40
+ })
41
+
42
+ Object.defineProperty(Kdu.prototype, '$route', {
43
+ get () { return this._routerRoot._route }
44
+ })
45
+
46
+ Kdu.component('router-view', View)
47
+ Kdu.component('router-link', Link)
48
+
49
+ const strats = Kdu.config.optionMergeStrategies
50
+ // use the same hook merging strategy for route hooks
51
+ strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
52
+ }
@@ -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,68 @@
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
+
9
+ export function normalizeLocation (
10
+ raw: RawLocation,
11
+ current: ?Route,
12
+ append: ?boolean,
13
+ router: ?KduRouter
14
+ ): Location {
15
+ let next: Location = typeof raw === 'string' ? { path: raw } : raw
16
+ // named target
17
+ if (next.name || next._normalized) {
18
+ return next
19
+ }
20
+
21
+ // relative params
22
+ if (!next.path && next.params && current) {
23
+ next = assign({}, next)
24
+ next._normalized = true
25
+ const params: any = assign(assign({}, current.params), next.params)
26
+ if (current.name) {
27
+ next.name = current.name
28
+ next.params = params
29
+ } else if (current.matched.length) {
30
+ const rawPath = current.matched[current.matched.length - 1].path
31
+ next.path = fillParams(rawPath, params, `path ${current.path}`)
32
+ } else if (process.env.NODE_ENV !== 'production') {
33
+ warn(false, `relative params navigation requires a current route.`)
34
+ }
35
+ return next
36
+ }
37
+
38
+ const parsedPath = parsePath(next.path || '')
39
+ const basePath = (current && current.path) || '/'
40
+ const path = parsedPath.path
41
+ ? resolvePath(parsedPath.path, basePath, append || next.append)
42
+ : basePath
43
+
44
+ const query = resolveQuery(
45
+ parsedPath.query,
46
+ next.query,
47
+ router && router.options.parseQuery
48
+ )
49
+
50
+ let hash = next.hash || parsedPath.hash
51
+ if (hash && hash.charAt(0) !== '#') {
52
+ hash = `#${hash}`
53
+ }
54
+
55
+ return {
56
+ _normalized: true,
57
+ path,
58
+ query,
59
+ hash
60
+ }
61
+ }
62
+
63
+ function assign (a, b) {
64
+ for (const key in b) {
65
+ a[key] = b[key]
66
+ }
67
+ return a
68
+ }
@@ -0,0 +1,26 @@
1
+ /* @flow */
2
+
3
+ import { warn } from './warn'
4
+ import Regexp from 'path-to-regexp'
5
+
6
+ const regexpCompileCache: {
7
+ [key: string]: Function
8
+ } = Object.create(null)
9
+
10
+ export function fillParams (
11
+ path: string,
12
+ params: ?Object,
13
+ routeMsg: string
14
+ ): string {
15
+ try {
16
+ const filler =
17
+ regexpCompileCache[path] ||
18
+ (regexpCompileCache[path] = Regexp.compile(path))
19
+ return filler(params || {}, { pretty: true })
20
+ } catch (e) {
21
+ if (process.env.NODE_ENV !== 'production') {
22
+ warn(false, `missing param for ${routeMsg}: ${e.message}`)
23
+ }
24
+ return ''
25
+ }
26
+ }
@@ -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,59 @@
1
+ /* @flow */
2
+
3
+ import { inBrowser } from './dom'
4
+ import { saveScrollPosition } from './scroll'
5
+
6
+ export const supportsPushState = inBrowser && (function () {
7
+ const ua = window.navigator.userAgent
8
+
9
+ if (
10
+ (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
11
+ ua.indexOf('Mobile Safari') !== -1 &&
12
+ ua.indexOf('Chrome') === -1 &&
13
+ ua.indexOf('Windows Phone') === -1
14
+ ) {
15
+ return false
16
+ }
17
+
18
+ return window.history && 'pushState' in window.history
19
+ })()
20
+
21
+ // use User Timing api (if present) for more accurate key precision
22
+ const Time = inBrowser && window.performance && window.performance.now
23
+ ? window.performance
24
+ : Date
25
+
26
+ let _key: string = genKey()
27
+
28
+ function genKey (): string {
29
+ return Time.now().toFixed(3)
30
+ }
31
+
32
+ export function getStateKey () {
33
+ return _key
34
+ }
35
+
36
+ export function setStateKey (key: string) {
37
+ _key = key
38
+ }
39
+
40
+ export function pushState (url?: string, replace?: boolean) {
41
+ saveScrollPosition()
42
+ // try...catch the pushState call to get around Safari
43
+ // DOM Exception 18 where it limits to 100 pushState calls
44
+ const history = window.history
45
+ try {
46
+ if (replace) {
47
+ history.replaceState({ key: _key }, '', url)
48
+ } else {
49
+ _key = genKey()
50
+ history.pushState({ key: _key }, '', url)
51
+ }
52
+ } catch (e) {
53
+ window.location[replace ? 'replace' : 'assign'](url)
54
+ }
55
+ }
56
+
57
+ export function replaceState (url?: string) {
58
+ pushState(url, true)
59
+ }
@@ -0,0 +1,96 @@
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
+ const val = extraQuery[key]
33
+ parsedQuery[key] = Array.isArray(val) ? val.slice() : val
34
+ }
35
+ return parsedQuery
36
+ }
37
+
38
+ function parseQuery (query: string): Dictionary<string> {
39
+ const res = {}
40
+
41
+ query = query.trim().replace(/^(\?|#|&)/, '')
42
+
43
+ if (!query) {
44
+ return res
45
+ }
46
+
47
+ query.split('&').forEach(param => {
48
+ const parts = param.replace(/\+/g, ' ').split('=')
49
+ const key = decode(parts.shift())
50
+ const val = parts.length > 0
51
+ ? decode(parts.join('='))
52
+ : null
53
+
54
+ if (res[key] === undefined) {
55
+ res[key] = val
56
+ } else if (Array.isArray(res[key])) {
57
+ res[key].push(val)
58
+ } else {
59
+ res[key] = [res[key], val]
60
+ }
61
+ })
62
+
63
+ return res
64
+ }
65
+
66
+ export function stringifyQuery (obj: Dictionary<string>): string {
67
+ const res = obj ? Object.keys(obj).map(key => {
68
+ const val = obj[key]
69
+
70
+ if (val === undefined) {
71
+ return ''
72
+ }
73
+
74
+ if (val === null) {
75
+ return encode(key)
76
+ }
77
+
78
+ if (Array.isArray(val)) {
79
+ const result = []
80
+ val.forEach(val2 => {
81
+ if (val2 === undefined) {
82
+ return
83
+ }
84
+ if (val2 === null) {
85
+ result.push(encode(key))
86
+ } else {
87
+ result.push(encode(key) + '=' + encode(val2))
88
+ }
89
+ })
90
+ return result.join('&')
91
+ }
92
+
93
+ return encode(key) + '=' + encode(val)
94
+ }).filter(x => x.length > 0).join('&') : null
95
+ return res ? `?${res}` : ''
96
+ }