kdu-router 3.1.3 → 3.4.0-beta.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.
@@ -0,0 +1,400 @@
1
+ /* @flow */
2
+
3
+ import { _Kdu } from '../install'
4
+ import type Router from '../index'
5
+ import { inBrowser } from '../util/dom'
6
+ import { runQueue } from '../util/async'
7
+ import { warn } from '../util/warn'
8
+ import { START, isSameRoute } from '../util/route'
9
+ import {
10
+ flatten,
11
+ flatMapComponents,
12
+ resolveAsyncComponents
13
+ } from '../util/resolve-components'
14
+ import {
15
+ createNavigationDuplicatedError,
16
+ createNavigationCancelledError,
17
+ createNavigationRedirectedError,
18
+ createNavigationAbortedError,
19
+ isError,
20
+ isNavigationFailure,
21
+ NavigationFailureType
22
+ } from '../util/errors'
23
+
24
+ export class History {
25
+ router: Router
26
+ base: string
27
+ current: Route
28
+ pending: ?Route
29
+ cb: (r: Route) => void
30
+ ready: boolean
31
+ readyCbs: Array<Function>
32
+ readyErrorCbs: Array<Function>
33
+ errorCbs: Array<Function>
34
+ listeners: Array<Function>
35
+ cleanupListeners: Function
36
+
37
+ // implemented by sub-classes
38
+ +go: (n: number) => void
39
+ +push: (loc: RawLocation, onComplete?: Function, onAbort?: Function) => void
40
+ +replace: (
41
+ loc: RawLocation,
42
+ onComplete?: Function,
43
+ onAbort?: Function
44
+ ) => void
45
+ +ensureURL: (push?: boolean) => void
46
+ +getCurrentLocation: () => string
47
+ +setupListeners: Function
48
+
49
+ constructor (router: Router, base: ?string) {
50
+ this.router = router
51
+ this.base = normalizeBase(base)
52
+ // start with a route object that stands for "nowhere"
53
+ this.current = START
54
+ this.pending = null
55
+ this.ready = false
56
+ this.readyCbs = []
57
+ this.readyErrorCbs = []
58
+ this.errorCbs = []
59
+ this.listeners = []
60
+ }
61
+
62
+ listen (cb: Function) {
63
+ this.cb = cb
64
+ }
65
+
66
+ onReady (cb: Function, errorCb: ?Function) {
67
+ if (this.ready) {
68
+ cb()
69
+ } else {
70
+ this.readyCbs.push(cb)
71
+ if (errorCb) {
72
+ this.readyErrorCbs.push(errorCb)
73
+ }
74
+ }
75
+ }
76
+
77
+ onError (errorCb: Function) {
78
+ this.errorCbs.push(errorCb)
79
+ }
80
+
81
+ transitionTo (
82
+ location: RawLocation,
83
+ onComplete?: Function,
84
+ onAbort?: Function
85
+ ) {
86
+ let route
87
+ try {
88
+ route = this.router.match(location, this.current)
89
+ } catch (e) {
90
+ this.errorCbs.forEach(cb => {
91
+ cb(e)
92
+ })
93
+ // Exception should still be thrown
94
+ throw e
95
+ }
96
+ this.confirmTransition(
97
+ route,
98
+ () => {
99
+ const prev = this.current
100
+ this.updateRoute(route)
101
+ onComplete && onComplete(route)
102
+ this.ensureURL()
103
+ this.router.afterHooks.forEach(hook => {
104
+ hook && hook(route, prev)
105
+ })
106
+
107
+ // fire ready cbs once
108
+ if (!this.ready) {
109
+ this.ready = true
110
+ this.readyCbs.forEach(cb => {
111
+ cb(route)
112
+ })
113
+ }
114
+ },
115
+ err => {
116
+ if (onAbort) {
117
+ onAbort(err)
118
+ }
119
+ if (err && !this.ready) {
120
+ this.ready = true
121
+ // Initial redirection should still trigger the onReady onSuccess
122
+ if (!isNavigationFailure(err, NavigationFailureType.redirected)) {
123
+ this.readyErrorCbs.forEach(cb => {
124
+ cb(err)
125
+ })
126
+ } else {
127
+ this.readyCbs.forEach(cb => {
128
+ cb(route)
129
+ })
130
+ }
131
+ }
132
+ }
133
+ )
134
+ }
135
+
136
+ confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
137
+ const current = this.current
138
+ const abort = err => {
139
+ // changed after adding errors before that change,
140
+ // redirect and aborted navigation would produce an err == null
141
+ if (!isNavigationFailure(err) && isError(err)) {
142
+ if (this.errorCbs.length) {
143
+ this.errorCbs.forEach(cb => {
144
+ cb(err)
145
+ })
146
+ } else {
147
+ warn(false, 'uncaught error during route navigation:')
148
+ console.error(err)
149
+ }
150
+ }
151
+ onAbort && onAbort(err)
152
+ }
153
+ const lastRouteIndex = route.matched.length - 1
154
+ const lastCurrentIndex = current.matched.length - 1
155
+ if (
156
+ isSameRoute(route, current) &&
157
+ // in the case the route map has been dynamically appended to
158
+ lastRouteIndex === lastCurrentIndex &&
159
+ route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
160
+ ) {
161
+ this.ensureURL()
162
+ return abort(createNavigationDuplicatedError(current, route))
163
+ }
164
+
165
+ const { updated, deactivated, activated } = resolveQueue(
166
+ this.current.matched,
167
+ route.matched
168
+ )
169
+
170
+ const queue: Array<?NavigationGuard> = [].concat(
171
+ // in-component leave guards
172
+ extractLeaveGuards(deactivated),
173
+ // global before hooks
174
+ this.router.beforeHooks,
175
+ // in-component update hooks
176
+ extractUpdateHooks(updated),
177
+ // in-config enter guards
178
+ activated.map(m => m.beforeEnter),
179
+ // async components
180
+ resolveAsyncComponents(activated)
181
+ )
182
+
183
+ this.pending = route
184
+ const iterator = (hook: NavigationGuard, next) => {
185
+ if (this.pending !== route) {
186
+ return abort(createNavigationCancelledError(current, route))
187
+ }
188
+ try {
189
+ hook(route, current, (to: any) => {
190
+ if (to === false) {
191
+ // next(false) -> abort navigation, ensure current URL
192
+ this.ensureURL(true)
193
+ abort(createNavigationAbortedError(current, route))
194
+ } else if (isError(to)) {
195
+ this.ensureURL(true)
196
+ abort(to)
197
+ } else if (
198
+ typeof to === 'string' ||
199
+ (typeof to === 'object' &&
200
+ (typeof to.path === 'string' || typeof to.name === 'string'))
201
+ ) {
202
+ // next('/') or next({ path: '/' }) -> redirect
203
+ abort(createNavigationRedirectedError(current, route))
204
+ if (typeof to === 'object' && to.replace) {
205
+ this.replace(to)
206
+ } else {
207
+ this.push(to)
208
+ }
209
+ } else {
210
+ // confirm transition and pass on the value
211
+ next(to)
212
+ }
213
+ })
214
+ } catch (e) {
215
+ abort(e)
216
+ }
217
+ }
218
+
219
+ runQueue(queue, iterator, () => {
220
+ const postEnterCbs = []
221
+ const isValid = () => this.current === route
222
+ // wait until async components are resolved before
223
+ // extracting in-component enter guards
224
+ const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
225
+ const queue = enterGuards.concat(this.router.resolveHooks)
226
+ runQueue(queue, iterator, () => {
227
+ if (this.pending !== route) {
228
+ return abort(createNavigationCancelledError(current, route))
229
+ }
230
+ this.pending = null
231
+ onComplete(route)
232
+ if (this.router.app) {
233
+ this.router.app.$nextTick(() => {
234
+ postEnterCbs.forEach(cb => {
235
+ cb()
236
+ })
237
+ })
238
+ }
239
+ })
240
+ })
241
+ }
242
+
243
+ updateRoute (route: Route) {
244
+ this.current = route
245
+ this.cb && this.cb(route)
246
+ }
247
+
248
+ setupListeners () {
249
+ // Default implementation is empty
250
+ }
251
+
252
+ teardownListeners () {
253
+ this.listeners.forEach(cleanupListener => {
254
+ cleanupListener()
255
+ })
256
+ this.listeners = []
257
+ }
258
+ }
259
+
260
+ function normalizeBase (base: ?string): string {
261
+ if (!base) {
262
+ if (inBrowser) {
263
+ // respect <base> tag
264
+ const baseEl = document.querySelector('base')
265
+ base = (baseEl && baseEl.getAttribute('href')) || '/'
266
+ // strip full URL origin
267
+ base = base.replace(/^https?:\/\/[^\/]+/, '')
268
+ } else {
269
+ base = '/'
270
+ }
271
+ }
272
+ // make sure there's the starting slash
273
+ if (base.charAt(0) !== '/') {
274
+ base = '/' + base
275
+ }
276
+ // remove trailing slash
277
+ return base.replace(/\/$/, '')
278
+ }
279
+
280
+ function resolveQueue (
281
+ current: Array<RouteRecord>,
282
+ next: Array<RouteRecord>
283
+ ): {
284
+ updated: Array<RouteRecord>,
285
+ activated: Array<RouteRecord>,
286
+ deactivated: Array<RouteRecord>
287
+ } {
288
+ let i
289
+ const max = Math.max(current.length, next.length)
290
+ for (i = 0; i < max; i++) {
291
+ if (current[i] !== next[i]) {
292
+ break
293
+ }
294
+ }
295
+ return {
296
+ updated: next.slice(0, i),
297
+ activated: next.slice(i),
298
+ deactivated: current.slice(i)
299
+ }
300
+ }
301
+
302
+ function extractGuards (
303
+ records: Array<RouteRecord>,
304
+ name: string,
305
+ bind: Function,
306
+ reverse?: boolean
307
+ ): Array<?Function> {
308
+ const guards = flatMapComponents(records, (def, instance, match, key) => {
309
+ const guard = extractGuard(def, name)
310
+ if (guard) {
311
+ return Array.isArray(guard)
312
+ ? guard.map(guard => bind(guard, instance, match, key))
313
+ : bind(guard, instance, match, key)
314
+ }
315
+ })
316
+ return flatten(reverse ? guards.reverse() : guards)
317
+ }
318
+
319
+ function extractGuard (
320
+ def: Object | Function,
321
+ key: string
322
+ ): NavigationGuard | Array<NavigationGuard> {
323
+ if (typeof def !== 'function') {
324
+ // extend now so that global mixins are applied.
325
+ def = _Kdu.extend(def)
326
+ }
327
+ return def.options[key]
328
+ }
329
+
330
+ function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
331
+ return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
332
+ }
333
+
334
+ function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
335
+ return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
336
+ }
337
+
338
+ function bindGuard (guard: NavigationGuard, instance: ?_Kdu): ?NavigationGuard {
339
+ if (instance) {
340
+ return function boundRouteGuard () {
341
+ return guard.apply(instance, arguments)
342
+ }
343
+ }
344
+ }
345
+
346
+ function extractEnterGuards (
347
+ activated: Array<RouteRecord>,
348
+ cbs: Array<Function>,
349
+ isValid: () => boolean
350
+ ): Array<?Function> {
351
+ return extractGuards(
352
+ activated,
353
+ 'beforeRouteEnter',
354
+ (guard, _, match, key) => {
355
+ return bindEnterGuard(guard, match, key, cbs, isValid)
356
+ }
357
+ )
358
+ }
359
+
360
+ function bindEnterGuard (
361
+ guard: NavigationGuard,
362
+ match: RouteRecord,
363
+ key: string,
364
+ cbs: Array<Function>,
365
+ isValid: () => boolean
366
+ ): NavigationGuard {
367
+ return function routeEnterGuard (to, from, next) {
368
+ return guard(to, from, cb => {
369
+ if (typeof cb === 'function') {
370
+ cbs.push(() => {
371
+ // #750
372
+ // if a router-view is wrapped with an out-in transition,
373
+ // the instance may not have been registered at this time.
374
+ // we will need to poll for registration until current route
375
+ // is no longer valid.
376
+ poll(cb, match.instances, key, isValid)
377
+ })
378
+ }
379
+ next(cb)
380
+ })
381
+ }
382
+ }
383
+
384
+ function poll (
385
+ cb: any, // somehow flow cannot infer this is a function
386
+ instances: Object,
387
+ key: string,
388
+ isValid: () => boolean
389
+ ) {
390
+ if (
391
+ instances[key] &&
392
+ !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
393
+ ) {
394
+ cb(instances[key])
395
+ } else if (isValid()) {
396
+ setTimeout(() => {
397
+ poll(cb, instances, key, isValid)
398
+ }, 16)
399
+ }
400
+ }
@@ -0,0 +1,163 @@
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
+ if (this.listeners.length > 0) {
24
+ return
25
+ }
26
+
27
+ const router = this.router
28
+ const expectScroll = router.options.scrollBehavior
29
+ const supportsScroll = supportsPushState && expectScroll
30
+
31
+ if (supportsScroll) {
32
+ this.listeners.push(setupScroll())
33
+ }
34
+
35
+ const handleRoutingEvent = () => {
36
+ const current = this.current
37
+ if (!ensureSlash()) {
38
+ return
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
53
+ )
54
+ this.listeners.push(() => {
55
+ window.removeEventListener(eventType, handleRoutingEvent)
56
+ })
57
+ }
58
+
59
+ push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
60
+ const { current: fromRoute } = this
61
+ this.transitionTo(
62
+ location,
63
+ route => {
64
+ pushHash(route.fullPath)
65
+ handleScroll(this.router, route, fromRoute, false)
66
+ onComplete && onComplete(route)
67
+ },
68
+ onAbort
69
+ )
70
+ }
71
+
72
+ replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
73
+ const { current: fromRoute } = this
74
+ this.transitionTo(
75
+ location,
76
+ route => {
77
+ replaceHash(route.fullPath)
78
+ handleScroll(this.router, route, fromRoute, false)
79
+ onComplete && onComplete(route)
80
+ },
81
+ onAbort
82
+ )
83
+ }
84
+
85
+ go (n: number) {
86
+ window.history.go(n)
87
+ }
88
+
89
+ ensureURL (push?: boolean) {
90
+ const current = this.current.fullPath
91
+ if (getHash() !== current) {
92
+ push ? pushHash(current) : replaceHash(current)
93
+ }
94
+ }
95
+
96
+ getCurrentLocation () {
97
+ return getHash()
98
+ }
99
+ }
100
+
101
+ function checkFallback (base) {
102
+ const location = getLocation(base)
103
+ if (!/^\/#/.test(location)) {
104
+ window.location.replace(cleanPath(base + '/#' + location))
105
+ return true
106
+ }
107
+ }
108
+
109
+ function ensureSlash (): boolean {
110
+ const path = getHash()
111
+ if (path.charAt(0) === '/') {
112
+ return true
113
+ }
114
+ replaceHash('/' + path)
115
+ return false
116
+ }
117
+
118
+ export function getHash (): string {
119
+ // We can't use window.location.hash here because it's not
120
+ // consistent across browsers - Firefox will pre-decode it!
121
+ let href = window.location.href
122
+ const index = href.indexOf('#')
123
+ // empty path
124
+ if (index < 0) return ''
125
+
126
+ href = href.slice(index + 1)
127
+ // decode the hash but not the search or hash
128
+ // as search(query) is already decoded
129
+ const searchIndex = href.indexOf('?')
130
+ if (searchIndex < 0) {
131
+ const hashIndex = href.indexOf('#')
132
+ if (hashIndex > -1) {
133
+ href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex)
134
+ } else href = decodeURI(href)
135
+ } else {
136
+ href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex)
137
+ }
138
+
139
+ return href
140
+ }
141
+
142
+ function getUrl (path) {
143
+ const href = window.location.href
144
+ const i = href.indexOf('#')
145
+ const base = i >= 0 ? href.slice(0, i) : href
146
+ return `${base}#${path}`
147
+ }
148
+
149
+ function pushHash (path) {
150
+ if (supportsPushState) {
151
+ pushState(getUrl(path))
152
+ } else {
153
+ window.location.hash = path
154
+ }
155
+ }
156
+
157
+ function replaceHash (path) {
158
+ if (supportsPushState) {
159
+ replaceState(getUrl(path))
160
+ } else {
161
+ window.location.replace(getUrl(path))
162
+ }
163
+ }
@@ -0,0 +1,94 @@
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
+ _startLocation: string
12
+
13
+ constructor (router: Router, base: ?string) {
14
+ super(router, base)
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
25
+ const expectScroll = router.options.scrollBehavior
26
+ const supportsScroll = supportsPushState && expectScroll
27
+
28
+ if (supportsScroll) {
29
+ this.listeners.push(setupScroll())
30
+ }
31
+
32
+ const handleRoutingEvent = () => {
33
+ const current = this.current
34
+
35
+ // Avoiding first `popstate` event dispatched in some browsers but first
36
+ // history route not updated since async guard at the same time.
37
+ const location = getLocation(this.base)
38
+ if (this.current === START && location === this._startLocation) {
39
+ return
40
+ }
41
+
42
+ this.transitionTo(location, route => {
43
+ if (supportsScroll) {
44
+ handleScroll(router, route, current, true)
45
+ }
46
+ })
47
+ }
48
+ window.addEventListener('popstate', handleRoutingEvent)
49
+ this.listeners.push(() => {
50
+ window.removeEventListener('popstate', handleRoutingEvent)
51
+ })
52
+ }
53
+
54
+ go (n: number) {
55
+ window.history.go(n)
56
+ }
57
+
58
+ push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
59
+ const { current: fromRoute } = this
60
+ this.transitionTo(location, route => {
61
+ pushState(cleanPath(this.base + route.fullPath))
62
+ handleScroll(this.router, route, fromRoute, false)
63
+ onComplete && onComplete(route)
64
+ }, onAbort)
65
+ }
66
+
67
+ replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
68
+ const { current: fromRoute } = this
69
+ this.transitionTo(location, route => {
70
+ replaceState(cleanPath(this.base + route.fullPath))
71
+ handleScroll(this.router, route, fromRoute, false)
72
+ onComplete && onComplete(route)
73
+ }, onAbort)
74
+ }
75
+
76
+ ensureURL (push?: boolean) {
77
+ if (getLocation(this.base) !== this.current.fullPath) {
78
+ const current = cleanPath(this.base + this.current.fullPath)
79
+ push ? pushState(current) : replaceState(current)
80
+ }
81
+ }
82
+
83
+ getCurrentLocation (): string {
84
+ return getLocation(this.base)
85
+ }
86
+ }
87
+
88
+ export function getLocation (base: string): string {
89
+ let path = decodeURI(window.location.pathname)
90
+ if (base && path.toLowerCase().indexOf(base.toLowerCase()) === 0) {
91
+ path = path.slice(base.length)
92
+ }
93
+ return (path || '/') + window.location.search + window.location.hash
94
+ }