kdu-router 3.1.3 → 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,352 @@
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, isError, isExtendedError } 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 { NavigationDuplicated } from './errors'
15
+
16
+ export class History {
17
+ router: Router
18
+ base: string
19
+ current: Route
20
+ pending: ?Route
21
+ cb: (r: Route) => void
22
+ ready: boolean
23
+ readyCbs: Array<Function>
24
+ readyErrorCbs: Array<Function>
25
+ errorCbs: Array<Function>
26
+
27
+ // implemented by sub-classes
28
+ +go: (n: number) => void
29
+ +push: (loc: RawLocation) => void
30
+ +replace: (loc: RawLocation) => void
31
+ +ensureURL: (push?: boolean) => void
32
+ +getCurrentLocation: () => string
33
+
34
+ constructor (router: Router, base: ?string) {
35
+ this.router = router
36
+ this.base = normalizeBase(base)
37
+ // start with a route object that stands for "nowhere"
38
+ this.current = START
39
+ this.pending = null
40
+ this.ready = false
41
+ this.readyCbs = []
42
+ this.readyErrorCbs = []
43
+ this.errorCbs = []
44
+ }
45
+
46
+ listen (cb: Function) {
47
+ this.cb = cb
48
+ }
49
+
50
+ onReady (cb: Function, errorCb: ?Function) {
51
+ if (this.ready) {
52
+ cb()
53
+ } else {
54
+ this.readyCbs.push(cb)
55
+ if (errorCb) {
56
+ this.readyErrorCbs.push(errorCb)
57
+ }
58
+ }
59
+ }
60
+
61
+ onError (errorCb: Function) {
62
+ this.errorCbs.push(errorCb)
63
+ }
64
+
65
+ transitionTo (
66
+ location: RawLocation,
67
+ onComplete?: Function,
68
+ onAbort?: Function
69
+ ) {
70
+ const route = this.router.match(location, this.current)
71
+ this.confirmTransition(
72
+ route,
73
+ () => {
74
+ this.updateRoute(route)
75
+ onComplete && onComplete(route)
76
+ this.ensureURL()
77
+
78
+ // fire ready cbs once
79
+ if (!this.ready) {
80
+ this.ready = true
81
+ this.readyCbs.forEach(cb => {
82
+ cb(route)
83
+ })
84
+ }
85
+ },
86
+ err => {
87
+ if (onAbort) {
88
+ onAbort(err)
89
+ }
90
+ if (err && !this.ready) {
91
+ this.ready = true
92
+ this.readyErrorCbs.forEach(cb => {
93
+ cb(err)
94
+ })
95
+ }
96
+ }
97
+ )
98
+ }
99
+
100
+ confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
101
+ const current = this.current
102
+ 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)) {
107
+ if (this.errorCbs.length) {
108
+ this.errorCbs.forEach(cb => {
109
+ cb(err)
110
+ })
111
+ } else {
112
+ warn(false, 'uncaught error during route navigation:')
113
+ console.error(err)
114
+ }
115
+ }
116
+ onAbort && onAbort(err)
117
+ }
118
+ if (
119
+ isSameRoute(route, current) &&
120
+ // in the case the route map has been dynamically appended to
121
+ route.matched.length === current.matched.length
122
+ ) {
123
+ this.ensureURL()
124
+ return abort(new NavigationDuplicated(route))
125
+ }
126
+
127
+ const { updated, deactivated, activated } = resolveQueue(
128
+ this.current.matched,
129
+ route.matched
130
+ )
131
+
132
+ const queue: Array<?NavigationGuard> = [].concat(
133
+ // in-component leave guards
134
+ extractLeaveGuards(deactivated),
135
+ // global before hooks
136
+ this.router.beforeHooks,
137
+ // in-component update hooks
138
+ extractUpdateHooks(updated),
139
+ // in-config enter guards
140
+ activated.map(m => m.beforeEnter),
141
+ // async components
142
+ resolveAsyncComponents(activated)
143
+ )
144
+
145
+ this.pending = route
146
+ const iterator = (hook: NavigationGuard, next) => {
147
+ if (this.pending !== route) {
148
+ return abort()
149
+ }
150
+ try {
151
+ hook(route, current, (to: any) => {
152
+ if (to === false || isError(to)) {
153
+ // next(false) -> abort navigation, ensure current URL
154
+ this.ensureURL(true)
155
+ abort(to)
156
+ } else if (
157
+ typeof to === 'string' ||
158
+ (typeof to === 'object' &&
159
+ (typeof to.path === 'string' || typeof to.name === 'string'))
160
+ ) {
161
+ // next('/') or next({ path: '/' }) -> redirect
162
+ abort()
163
+ if (typeof to === 'object' && to.replace) {
164
+ this.replace(to)
165
+ } else {
166
+ this.push(to)
167
+ }
168
+ } else {
169
+ // confirm transition and pass on the value
170
+ next(to)
171
+ }
172
+ })
173
+ } catch (e) {
174
+ abort(e)
175
+ }
176
+ }
177
+
178
+ runQueue(queue, iterator, () => {
179
+ const postEnterCbs = []
180
+ const isValid = () => this.current === route
181
+ // wait until async components are resolved before
182
+ // extracting in-component enter guards
183
+ const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
184
+ const queue = enterGuards.concat(this.router.resolveHooks)
185
+ runQueue(queue, iterator, () => {
186
+ if (this.pending !== route) {
187
+ return abort()
188
+ }
189
+ this.pending = null
190
+ onComplete(route)
191
+ if (this.router.app) {
192
+ this.router.app.$nextTick(() => {
193
+ postEnterCbs.forEach(cb => {
194
+ cb()
195
+ })
196
+ })
197
+ }
198
+ })
199
+ })
200
+ }
201
+
202
+ updateRoute (route: Route) {
203
+ const prev = this.current
204
+ this.current = route
205
+ this.cb && this.cb(route)
206
+ this.router.afterHooks.forEach(hook => {
207
+ hook && hook(route, prev)
208
+ })
209
+ }
210
+ }
211
+
212
+ function normalizeBase (base: ?string): string {
213
+ if (!base) {
214
+ if (inBrowser) {
215
+ // respect <base> tag
216
+ const baseEl = document.querySelector('base')
217
+ base = (baseEl && baseEl.getAttribute('href')) || '/'
218
+ // strip full URL origin
219
+ base = base.replace(/^https?:\/\/[^\/]+/, '')
220
+ } else {
221
+ base = '/'
222
+ }
223
+ }
224
+ // make sure there's the starting slash
225
+ if (base.charAt(0) !== '/') {
226
+ base = '/' + base
227
+ }
228
+ // remove trailing slash
229
+ return base.replace(/\/$/, '')
230
+ }
231
+
232
+ function resolveQueue (
233
+ current: Array<RouteRecord>,
234
+ next: Array<RouteRecord>
235
+ ): {
236
+ updated: Array<RouteRecord>,
237
+ activated: Array<RouteRecord>,
238
+ deactivated: Array<RouteRecord>
239
+ } {
240
+ let i
241
+ const max = Math.max(current.length, next.length)
242
+ for (i = 0; i < max; i++) {
243
+ if (current[i] !== next[i]) {
244
+ break
245
+ }
246
+ }
247
+ return {
248
+ updated: next.slice(0, i),
249
+ activated: next.slice(i),
250
+ deactivated: current.slice(i)
251
+ }
252
+ }
253
+
254
+ function extractGuards (
255
+ records: Array<RouteRecord>,
256
+ name: string,
257
+ bind: Function,
258
+ reverse?: boolean
259
+ ): Array<?Function> {
260
+ const guards = flatMapComponents(records, (def, instance, match, key) => {
261
+ const guard = extractGuard(def, name)
262
+ if (guard) {
263
+ return Array.isArray(guard)
264
+ ? guard.map(guard => bind(guard, instance, match, key))
265
+ : bind(guard, instance, match, key)
266
+ }
267
+ })
268
+ return flatten(reverse ? guards.reverse() : guards)
269
+ }
270
+
271
+ function extractGuard (
272
+ def: Object | Function,
273
+ key: string
274
+ ): NavigationGuard | Array<NavigationGuard> {
275
+ if (typeof def !== 'function') {
276
+ // extend now so that global mixins are applied.
277
+ def = _Kdu.extend(def)
278
+ }
279
+ return def.options[key]
280
+ }
281
+
282
+ function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
283
+ return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
284
+ }
285
+
286
+ function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
287
+ return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
288
+ }
289
+
290
+ function bindGuard (guard: NavigationGuard, instance: ?_Kdu): ?NavigationGuard {
291
+ if (instance) {
292
+ return function boundRouteGuard () {
293
+ return guard.apply(instance, arguments)
294
+ }
295
+ }
296
+ }
297
+
298
+ function extractEnterGuards (
299
+ activated: Array<RouteRecord>,
300
+ cbs: Array<Function>,
301
+ isValid: () => boolean
302
+ ): Array<?Function> {
303
+ return extractGuards(
304
+ activated,
305
+ 'beforeRouteEnter',
306
+ (guard, _, match, key) => {
307
+ return bindEnterGuard(guard, match, key, cbs, isValid)
308
+ }
309
+ )
310
+ }
311
+
312
+ function bindEnterGuard (
313
+ guard: NavigationGuard,
314
+ match: RouteRecord,
315
+ key: string,
316
+ cbs: Array<Function>,
317
+ isValid: () => boolean
318
+ ): NavigationGuard {
319
+ return function routeEnterGuard (to, from, next) {
320
+ return guard(to, from, cb => {
321
+ 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
+ })
330
+ }
331
+ next(cb)
332
+ })
333
+ }
334
+ }
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
+ }
@@ -0,0 +1,22 @@
1
+ export class NavigationDuplicated extends Error {
2
+ constructor (normalizedLocation) {
3
+ super()
4
+ this.name = this._name = 'NavigationDuplicated'
5
+ // passing the message to super() doesn't seem to work in the transpiled version
6
+ this.message = `Navigating to current location ("${
7
+ normalizedLocation.fullPath
8
+ }") is not allowed`
9
+ // add a stack property so services like Sentry can correctly display it
10
+ Object.defineProperty(this, 'stack', {
11
+ value: new Error().stack,
12
+ writable: true,
13
+ configurable: true
14
+ })
15
+ // we could also have used
16
+ // Error.captureStackTrace(this, this.constructor)
17
+ // but it only exists on node and chrome
18
+ }
19
+ }
20
+
21
+ // support IE9
22
+ NavigationDuplicated._name = 'NavigationDuplicated'
@@ -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
+ }