kdu-router 3.1.7 → 3.5.4

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.
@@ -4,14 +4,23 @@ import { _Kdu } from '../install'
4
4
  import type Router from '../index'
5
5
  import { inBrowser } from '../util/dom'
6
6
  import { runQueue } from '../util/async'
7
- import { warn, isError, isExtendedError } from '../util/warn'
8
- import { START, isSameRoute } from '../util/route'
7
+ import { warn } from '../util/warn'
8
+ import { START, isSameRoute, handleRouteEntered } from '../util/route'
9
9
  import {
10
10
  flatten,
11
11
  flatMapComponents,
12
12
  resolveAsyncComponents
13
13
  } from '../util/resolve-components'
14
- import { NavigationDuplicated } from './errors'
14
+ import {
15
+ createNavigationDuplicatedError,
16
+ createNavigationCancelledError,
17
+ createNavigationRedirectedError,
18
+ createNavigationAbortedError,
19
+ isError,
20
+ isNavigationFailure,
21
+ NavigationFailureType
22
+ } from '../util/errors'
23
+ import { handleScroll } from '../util/scroll'
15
24
 
16
25
  export class History {
17
26
  router: Router
@@ -23,13 +32,20 @@ export class History {
23
32
  readyCbs: Array<Function>
24
33
  readyErrorCbs: Array<Function>
25
34
  errorCbs: Array<Function>
35
+ listeners: Array<Function>
36
+ cleanupListeners: Function
26
37
 
27
38
  // implemented by sub-classes
28
39
  +go: (n: number) => void
29
- +push: (loc: RawLocation) => void
30
- +replace: (loc: RawLocation) => void
40
+ +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
41
+ +replace: (
42
+ loc: RawLocation,
43
+ onComplete?: Function,
44
+ onAbort?: Function
45
+ ) => void
31
46
  +ensureURL: (push?: boolean) => void
32
47
  +getCurrentLocation: () => string
48
+ +setupListeners: Function
33
49
 
34
50
  constructor (router: Router, base: ?string) {
35
51
  this.router = router
@@ -41,6 +57,7 @@ export class History {
41
57
  this.readyCbs = []
42
58
  this.readyErrorCbs = []
43
59
  this.errorCbs = []
60
+ this.listeners = []
44
61
  }
45
62
 
46
63
  listen (cb: Function) {
@@ -67,13 +84,27 @@ export class History {
67
84
  onComplete?: Function,
68
85
  onAbort?: Function
69
86
  ) {
70
- const route = this.router.match(location, this.current)
87
+ let route
88
+ // catch redirect option
89
+ try {
90
+ route = this.router.match(location, this.current)
91
+ } catch (e) {
92
+ this.errorCbs.forEach(cb => {
93
+ cb(e)
94
+ })
95
+ // Exception should still be thrown
96
+ throw e
97
+ }
98
+ const prev = this.current
71
99
  this.confirmTransition(
72
100
  route,
73
101
  () => {
74
102
  this.updateRoute(route)
75
103
  onComplete && onComplete(route)
76
104
  this.ensureURL()
105
+ this.router.afterHooks.forEach(hook => {
106
+ hook && hook(route, prev)
107
+ })
77
108
 
78
109
  // fire ready cbs once
79
110
  if (!this.ready) {
@@ -88,10 +119,14 @@ export class History {
88
119
  onAbort(err)
89
120
  }
90
121
  if (err && !this.ready) {
91
- this.ready = true
92
- this.readyErrorCbs.forEach(cb => {
93
- cb(err)
94
- })
122
+ // Initial redirection should not mark the history as ready yet
123
+ // because it's triggered by the redirection instead
124
+ if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
125
+ this.ready = true
126
+ this.readyErrorCbs.forEach(cb => {
127
+ cb(err)
128
+ })
129
+ }
95
130
  }
96
131
  }
97
132
  )
@@ -99,29 +134,37 @@ export class History {
99
134
 
100
135
  confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
101
136
  const current = this.current
137
+ this.pending = route
102
138
  const abort = err => {
103
- // When the user navigates through history through back/forward buttons
104
- // we do not want to throw the error. We only throw it if directly calling
105
- // push/replace. That's why it's not included in isError
106
- if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
139
+ // changed after adding errors
140
+ // before that change, redirect and aborted navigation would produce an err == null
141
+ if (!isNavigationFailure(err) && isError(err)) {
107
142
  if (this.errorCbs.length) {
108
143
  this.errorCbs.forEach(cb => {
109
144
  cb(err)
110
145
  })
111
146
  } else {
112
- warn(false, 'uncaught error during route navigation:')
147
+ if (process.env.NODE_ENV !== 'production') {
148
+ warn(false, 'uncaught error during route navigation:')
149
+ }
113
150
  console.error(err)
114
151
  }
115
152
  }
116
153
  onAbort && onAbort(err)
117
154
  }
155
+ const lastRouteIndex = route.matched.length - 1
156
+ const lastCurrentIndex = current.matched.length - 1
118
157
  if (
119
158
  isSameRoute(route, current) &&
120
159
  // in the case the route map has been dynamically appended to
121
- route.matched.length === current.matched.length
160
+ lastRouteIndex === lastCurrentIndex &&
161
+ route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
122
162
  ) {
123
163
  this.ensureURL()
124
- return abort(new NavigationDuplicated(route))
164
+ if (route.hash) {
165
+ handleScroll(this.router, current, route, false)
166
+ }
167
+ return abort(createNavigationDuplicatedError(current, route))
125
168
  }
126
169
 
127
170
  const { updated, deactivated, activated } = resolveQueue(
@@ -142,15 +185,17 @@ export class History {
142
185
  resolveAsyncComponents(activated)
143
186
  )
144
187
 
145
- this.pending = route
146
188
  const iterator = (hook: NavigationGuard, next) => {
147
189
  if (this.pending !== route) {
148
- return abort()
190
+ return abort(createNavigationCancelledError(current, route))
149
191
  }
150
192
  try {
151
193
  hook(route, current, (to: any) => {
152
- if (to === false || isError(to)) {
194
+ if (to === false) {
153
195
  // next(false) -> abort navigation, ensure current URL
196
+ this.ensureURL(true)
197
+ abort(createNavigationAbortedError(current, route))
198
+ } else if (isError(to)) {
154
199
  this.ensureURL(true)
155
200
  abort(to)
156
201
  } else if (
@@ -159,7 +204,7 @@ export class History {
159
204
  (typeof to.path === 'string' || typeof to.name === 'string'))
160
205
  ) {
161
206
  // next('/') or next({ path: '/' }) -> redirect
162
- abort()
207
+ abort(createNavigationRedirectedError(current, route))
163
208
  if (typeof to === 'object' && to.replace) {
164
209
  this.replace(to)
165
210
  } else {
@@ -176,23 +221,19 @@ export class History {
176
221
  }
177
222
 
178
223
  runQueue(queue, iterator, () => {
179
- const postEnterCbs = []
180
- const isValid = () => this.current === route
181
224
  // wait until async components are resolved before
182
225
  // extracting in-component enter guards
183
- const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
226
+ const enterGuards = extractEnterGuards(activated)
184
227
  const queue = enterGuards.concat(this.router.resolveHooks)
185
228
  runQueue(queue, iterator, () => {
186
229
  if (this.pending !== route) {
187
- return abort()
230
+ return abort(createNavigationCancelledError(current, route))
188
231
  }
189
232
  this.pending = null
190
233
  onComplete(route)
191
234
  if (this.router.app) {
192
235
  this.router.app.$nextTick(() => {
193
- postEnterCbs.forEach(cb => {
194
- cb()
195
- })
236
+ handleRouteEntered(route)
196
237
  })
197
238
  }
198
239
  })
@@ -200,12 +241,24 @@ export class History {
200
241
  }
201
242
 
202
243
  updateRoute (route: Route) {
203
- const prev = this.current
204
244
  this.current = route
205
245
  this.cb && this.cb(route)
206
- this.router.afterHooks.forEach(hook => {
207
- hook && hook(route, prev)
246
+ }
247
+
248
+ setupListeners () {
249
+ // Default implementation is empty
250
+ }
251
+
252
+ teardown () {
253
+ // clean up event listeners
254
+ this.listeners.forEach(cleanupListener => {
255
+ cleanupListener()
208
256
  })
257
+ this.listeners = []
258
+
259
+ // reset current history route
260
+ this.current = START
261
+ this.pending = null
209
262
  }
210
263
  }
211
264
 
@@ -296,15 +349,13 @@ function bindGuard (guard: NavigationGuard, instance: ?_Kdu): ?NavigationGuard {
296
349
  }
297
350
 
298
351
  function extractEnterGuards (
299
- activated: Array<RouteRecord>,
300
- cbs: Array<Function>,
301
- isValid: () => boolean
352
+ activated: Array<RouteRecord>
302
353
  ): Array<?Function> {
303
354
  return extractGuards(
304
355
  activated,
305
356
  'beforeRouteEnter',
306
357
  (guard, _, match, key) => {
307
- return bindEnterGuard(guard, match, key, cbs, isValid)
358
+ return bindEnterGuard(guard, match, key)
308
359
  }
309
360
  )
310
361
  }
@@ -312,41 +363,17 @@ function extractEnterGuards (
312
363
  function bindEnterGuard (
313
364
  guard: NavigationGuard,
314
365
  match: RouteRecord,
315
- key: string,
316
- cbs: Array<Function>,
317
- isValid: () => boolean
366
+ key: string
318
367
  ): NavigationGuard {
319
368
  return function routeEnterGuard (to, from, next) {
320
369
  return guard(to, from, cb => {
321
370
  if (typeof cb === 'function') {
322
- cbs.push(() => {
323
- // #750
324
- // if a router-view is wrapped with an out-in transition,
325
- // the instance may not have been registered at this time.
326
- // we will need to poll for registration until current route
327
- // is no longer valid.
328
- poll(cb, match.instances, key, isValid)
329
- })
371
+ if (!match.enteredCbs[key]) {
372
+ match.enteredCbs[key] = []
373
+ }
374
+ match.enteredCbs[key].push(cb)
330
375
  }
331
376
  next(cb)
332
377
  })
333
378
  }
334
379
  }
335
-
336
- function poll (
337
- cb: any, // somehow flow cannot infer this is a function
338
- instances: Object,
339
- key: string,
340
- isValid: () => boolean
341
- ) {
342
- if (
343
- instances[key] &&
344
- !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
345
- ) {
346
- cb(instances[key])
347
- } else if (isValid()) {
348
- setTimeout(() => {
349
- poll(cb, instances, key, isValid)
350
- }, 16)
351
- }
352
- }
@@ -20,31 +20,40 @@ export class HashHistory extends History {
20
20
  // this is delayed until the app mounts
21
21
  // to avoid the hashchange listener being fired too early
22
22
  setupListeners () {
23
+ if (this.listeners.length > 0) {
24
+ return
25
+ }
26
+
23
27
  const router = this.router
24
28
  const expectScroll = router.options.scrollBehavior
25
29
  const supportsScroll = supportsPushState && expectScroll
26
30
 
27
31
  if (supportsScroll) {
28
- setupScroll()
32
+ this.listeners.push(setupScroll())
29
33
  }
30
34
 
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
- })
35
+ const handleRoutingEvent = () => {
36
+ const current = this.current
37
+ if (!ensureSlash()) {
38
+ return
46
39
  }
40
+ this.transitionTo(getHash(), route => {
41
+ if (supportsScroll) {
42
+ handleScroll(this.router, route, current, true)
43
+ }
44
+ if (!supportsPushState) {
45
+ replaceHash(route.fullPath)
46
+ }
47
+ })
48
+ }
49
+ const eventType = supportsPushState ? 'popstate' : 'hashchange'
50
+ window.addEventListener(
51
+ eventType,
52
+ handleRoutingEvent
47
53
  )
54
+ this.listeners.push(() => {
55
+ window.removeEventListener(eventType, handleRoutingEvent)
56
+ })
48
57
  }
49
58
 
50
59
  push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
@@ -115,17 +124,6 @@ export function getHash (): string {
115
124
  if (index < 0) return ''
116
125
 
117
126
  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
127
 
130
128
  return href
131
129
  }
@@ -8,24 +8,34 @@ import { setupScroll, handleScroll } from '../util/scroll'
8
8
  import { pushState, replaceState, supportsPushState } from '../util/push-state'
9
9
 
10
10
  export class HTML5History extends History {
11
+ _startLocation: string
12
+
11
13
  constructor (router: Router, base: ?string) {
12
14
  super(router, base)
13
15
 
16
+ this._startLocation = getLocation(this.base)
17
+ }
18
+
19
+ setupListeners () {
20
+ if (this.listeners.length > 0) {
21
+ return
22
+ }
23
+
24
+ const router = this.router
14
25
  const expectScroll = router.options.scrollBehavior
15
26
  const supportsScroll = supportsPushState && expectScroll
16
27
 
17
28
  if (supportsScroll) {
18
- setupScroll()
29
+ this.listeners.push(setupScroll())
19
30
  }
20
31
 
21
- const initLocation = getLocation(this.base)
22
- window.addEventListener('popstate', e => {
32
+ const handleRoutingEvent = () => {
23
33
  const current = this.current
24
34
 
25
35
  // Avoiding first `popstate` event dispatched in some browsers but first
26
36
  // history route not updated since async guard at the same time.
27
37
  const location = getLocation(this.base)
28
- if (this.current === START && location === initLocation) {
38
+ if (this.current === START && location === this._startLocation) {
29
39
  return
30
40
  }
31
41
 
@@ -34,6 +44,10 @@ export class HTML5History extends History {
34
44
  handleScroll(router, route, current, true)
35
45
  }
36
46
  })
47
+ }
48
+ window.addEventListener('popstate', handleRoutingEvent)
49
+ this.listeners.push(() => {
50
+ window.removeEventListener('popstate', handleRoutingEvent)
37
51
  })
38
52
  }
39
53
 
@@ -72,8 +86,13 @@ export class HTML5History extends History {
72
86
  }
73
87
 
74
88
  export function getLocation (base: string): string {
75
- let path = decodeURI(window.location.pathname)
76
- if (base && path.indexOf(base) === 0) {
89
+ let path = window.location.pathname
90
+ const pathLowerCase = path.toLowerCase()
91
+ const baseLowerCase = base.toLowerCase()
92
+ // base="/a" shouldn't turn path="/app" into "/a/pp"
93
+ // so we ensure the trailing slash in the base
94
+ if (base && ((pathLowerCase === baseLowerCase) ||
95
+ (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
77
96
  path = path.slice(base.length)
78
97
  }
79
98
  return (path || '/') + window.location.search + window.location.hash
package/src/index.js CHANGED
@@ -2,12 +2,13 @@
2
2
 
3
3
  import { install } from './install'
4
4
  import { START } from './util/route'
5
- import { assert } from './util/warn'
5
+ import { assert, warn } from './util/warn'
6
6
  import { inBrowser } from './util/dom'
7
7
  import { cleanPath } from './util/path'
8
8
  import { createMatcher } from './create-matcher'
9
9
  import { normalizeLocation } from './util/location'
10
10
  import { supportsPushState } from './util/push-state'
11
+ import { handleScroll } from './util/scroll'
11
12
 
12
13
  import { HashHistory } from './history/hash'
13
14
  import { HTML5History } from './history/html5'
@@ -15,24 +16,32 @@ import { AbstractHistory } from './history/abstract'
15
16
 
16
17
  import type { Matcher } from './create-matcher'
17
18
 
19
+ import { isNavigationFailure, NavigationFailureType } from './util/errors'
20
+
18
21
  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>;
22
+ static install: () => void
23
+ static version: string
24
+ static isNavigationFailure: Function
25
+ static NavigationFailureType: any
26
+ static START_LOCATION: Route
27
+
28
+ app: any
29
+ apps: Array<any>
30
+ ready: boolean
31
+ readyCbs: Array<Function>
32
+ options: RouterOptions
33
+ mode: string
34
+ history: HashHistory | HTML5History | AbstractHistory
35
+ matcher: Matcher
36
+ fallback: boolean
37
+ beforeHooks: Array<?NavigationGuard>
38
+ resolveHooks: Array<?NavigationGuard>
39
+ afterHooks: Array<?AfterNavigationHook>
34
40
 
35
41
  constructor (options: RouterOptions = {}) {
42
+ if (process.env.NODE_ENV !== 'production') {
43
+ warn(this instanceof KduRouter, `Router must be called with the new operator.`)
44
+ }
36
45
  this.app = null
37
46
  this.apps = []
38
47
  this.options = options
@@ -42,7 +51,8 @@ export default class KduRouter {
42
51
  this.matcher = createMatcher(options.routes || [], this)
43
52
 
44
53
  let mode = options.mode || 'hash'
45
- this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
54
+ this.fallback =
55
+ mode === 'history' && !supportsPushState && options.fallback !== false
46
56
  if (this.fallback) {
47
57
  mode = 'hash'
48
58
  }
@@ -68,11 +78,7 @@ export default class KduRouter {
68
78
  }
69
79
  }
70
80
 
71
- match (
72
- raw: RawLocation,
73
- current?: Route,
74
- redirectedFrom?: Location
75
- ): Route {
81
+ match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
76
82
  return this.matcher.match(raw, current, redirectedFrom)
77
83
  }
78
84
 
@@ -81,11 +87,12 @@ export default class KduRouter {
81
87
  }
82
88
 
83
89
  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
- )
90
+ process.env.NODE_ENV !== 'production' &&
91
+ assert(
92
+ install.installed,
93
+ `not installed. Make sure to call \`Kdu.use(KduRouter)\` ` +
94
+ `before creating root instance.`
95
+ )
89
96
 
90
97
  this.apps.push(app)
91
98
 
@@ -97,6 +104,8 @@ export default class KduRouter {
97
104
  // ensure we still have a main app or null if no apps
98
105
  // we do not release the router so it can be reused
99
106
  if (this.app === app) this.app = this.apps[0] || null
107
+
108
+ if (!this.app) this.history.teardown()
100
109
  })
101
110
 
102
111
  // main app previously initialized
@@ -109,21 +118,29 @@ export default class KduRouter {
109
118
 
110
119
  const history = this.history
111
120
 
112
- if (history instanceof HTML5History) {
113
- history.transitionTo(history.getCurrentLocation())
114
- } else if (history instanceof HashHistory) {
115
- const setupHashListener = () => {
121
+ if (history instanceof HTML5History || history instanceof HashHistory) {
122
+ const handleInitialScroll = routeOrError => {
123
+ const from = history.current
124
+ const expectScroll = this.options.scrollBehavior
125
+ const supportsScroll = supportsPushState && expectScroll
126
+
127
+ if (supportsScroll && 'fullPath' in routeOrError) {
128
+ handleScroll(this, routeOrError, from, false)
129
+ }
130
+ }
131
+ const setupListeners = routeOrError => {
116
132
  history.setupListeners()
133
+ handleInitialScroll(routeOrError)
117
134
  }
118
135
  history.transitionTo(
119
136
  history.getCurrentLocation(),
120
- setupHashListener,
121
- setupHashListener
137
+ setupListeners,
138
+ setupListeners
122
139
  )
123
140
  }
124
141
 
125
142
  history.listen(route => {
126
- this.apps.forEach((app) => {
143
+ this.apps.forEach(app => {
127
144
  app._route = route
128
145
  })
129
146
  })
@@ -192,11 +209,14 @@ export default class KduRouter {
192
209
  if (!route) {
193
210
  return []
194
211
  }
195
- return [].concat.apply([], route.matched.map(m => {
196
- return Object.keys(m.components).map(key => {
197
- return m.components[key]
212
+ return [].concat.apply(
213
+ [],
214
+ route.matched.map(m => {
215
+ return Object.keys(m.components).map(key => {
216
+ return m.components[key]
217
+ })
198
218
  })
199
- }))
219
+ )
200
220
  }
201
221
 
202
222
  resolve (
@@ -212,12 +232,7 @@ export default class KduRouter {
212
232
  resolved: Route
213
233
  } {
214
234
  current = current || this.history.current
215
- const location = normalizeLocation(
216
- to,
217
- current,
218
- append,
219
- this
220
- )
235
+ const location = normalizeLocation(to, current, append, this)
221
236
  const route = this.match(location, current)
222
237
  const fullPath = route.redirectedFrom || route.fullPath
223
238
  const base = this.history.base
@@ -232,7 +247,21 @@ export default class KduRouter {
232
247
  }
233
248
  }
234
249
 
250
+ getRoutes () {
251
+ return this.matcher.getRoutes()
252
+ }
253
+
254
+ addRoute (parentOrRoute: string | RouteConfig, route?: RouteConfig) {
255
+ this.matcher.addRoute(parentOrRoute, route)
256
+ if (this.history.current !== START) {
257
+ this.history.transitionTo(this.history.getCurrentLocation())
258
+ }
259
+ }
260
+
235
261
  addRoutes (routes: Array<RouteConfig>) {
262
+ if (process.env.NODE_ENV !== 'production') {
263
+ warn(false, 'router.addRoutes() is deprecated and has been removed in Kdu Router 4. Use router.addRoute() instead.')
264
+ }
236
265
  this.matcher.addRoutes(routes)
237
266
  if (this.history.current !== START) {
238
267
  this.history.transitionTo(this.history.getCurrentLocation())
@@ -255,6 +284,9 @@ function createHref (base: string, fullPath: string, mode) {
255
284
 
256
285
  KduRouter.install = install
257
286
  KduRouter.version = '__VERSION__'
287
+ KduRouter.isNavigationFailure = isNavigationFailure
288
+ KduRouter.NavigationFailureType = NavigationFailureType
289
+ KduRouter.START_LOCATION = START
258
290
 
259
291
  if (inBrowser && window.Kdu) {
260
292
  window.Kdu.use(KduRouter)