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.
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * kdu-router v3.1.7
2
+ * kdu-router v3.5.4
3
3
  * (c) 2022 NKDuy
4
4
  * @license MIT
5
5
  */
@@ -12,23 +12,11 @@ function assert (condition, message) {
12
12
  }
13
13
 
14
14
  function warn (condition, message) {
15
- if ( !condition) {
15
+ if (!condition) {
16
16
  typeof console !== 'undefined' && console.warn(`[kdu-router] ${message}`);
17
17
  }
18
18
  }
19
19
 
20
- function isError (err) {
21
- return Object.prototype.toString.call(err).indexOf('Error') > -1
22
- }
23
-
24
- function isExtendedError (constructor, err) {
25
- return (
26
- err instanceof constructor ||
27
- // _name is to support IE9 too
28
- (err && (err.name === constructor.name || err._name === constructor._name))
29
- )
30
- }
31
-
32
20
  function extend (a, b) {
33
21
  for (const key in b) {
34
22
  a[key] = b[key];
@@ -36,153 +24,6 @@ function extend (a, b) {
36
24
  return a
37
25
  }
38
26
 
39
- var View = {
40
- name: 'RouterView',
41
- functional: true,
42
- props: {
43
- name: {
44
- type: String,
45
- default: 'default'
46
- }
47
- },
48
- render (_, { props, children, parent, data }) {
49
- // used by devtools to display a router-view badge
50
- data.routerView = true;
51
-
52
- // directly use parent context's createElement() function
53
- // so that components rendered by router-view can resolve named slots
54
- const h = parent.$createElement;
55
- const name = props.name;
56
- const route = parent.$route;
57
- const cache = parent._routerViewCache || (parent._routerViewCache = {});
58
-
59
- // determine current view depth, also check to see if the tree
60
- // has been toggled inactive but kept-alive.
61
- let depth = 0;
62
- let inactive = false;
63
- while (parent && parent._routerRoot !== parent) {
64
- const knodeData = parent.$knode ? parent.$knode.data : {};
65
- if (knodeData.routerView) {
66
- depth++;
67
- }
68
- if (knodeData.keepAlive && parent._directInactive && parent._inactive) {
69
- inactive = true;
70
- }
71
- parent = parent.$parent;
72
- }
73
- data.routerViewDepth = depth;
74
-
75
- // render previous view if the tree is inactive and kept-alive
76
- if (inactive) {
77
- const cachedData = cache[name];
78
- const cachedComponent = cachedData && cachedData.component;
79
- if (cachedComponent) {
80
- // #2301
81
- // pass props
82
- if (cachedData.configProps) {
83
- fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps);
84
- }
85
- return h(cachedComponent, data, children)
86
- } else {
87
- // render previous empty view
88
- return h()
89
- }
90
- }
91
-
92
- const matched = route.matched[depth];
93
- const component = matched && matched.components[name];
94
-
95
- // render empty node if no matched route or no config component
96
- if (!matched || !component) {
97
- cache[name] = null;
98
- return h()
99
- }
100
-
101
- // cache component
102
- cache[name] = { component };
103
-
104
- // attach instance registration hook
105
- // this will be called in the instance's injected lifecycle hooks
106
- data.registerRouteInstance = (vm, val) => {
107
- // val could be undefined for unregistration
108
- const current = matched.instances[name];
109
- if (
110
- (val && current !== vm) ||
111
- (!val && current === vm)
112
- ) {
113
- matched.instances[name] = val;
114
- }
115
- }
116
-
117
- // also register instance in prepatch hook
118
- // in case the same component instance is reused across different routes
119
- ;(data.hook || (data.hook = {})).prepatch = (_, knode) => {
120
- matched.instances[name] = knode.componentInstance;
121
- };
122
-
123
- // register instance in init hook
124
- // in case kept-alive component be actived when routes changed
125
- data.hook.init = (knode) => {
126
- if (knode.data.keepAlive &&
127
- knode.componentInstance &&
128
- knode.componentInstance !== matched.instances[name]
129
- ) {
130
- matched.instances[name] = knode.componentInstance;
131
- }
132
- };
133
-
134
- const configProps = matched.props && matched.props[name];
135
- // save route and configProps in cachce
136
- if (configProps) {
137
- extend(cache[name], {
138
- route,
139
- configProps
140
- });
141
- fillPropsinData(component, data, route, configProps);
142
- }
143
-
144
- return h(component, data, children)
145
- }
146
- };
147
-
148
- function fillPropsinData (component, data, route, configProps) {
149
- // resolve props
150
- let propsToPass = data.props = resolveProps(route, configProps);
151
- if (propsToPass) {
152
- // clone to prevent mutation
153
- propsToPass = data.props = extend({}, propsToPass);
154
- // pass non-declared props as attrs
155
- const attrs = data.attrs = data.attrs || {};
156
- for (const key in propsToPass) {
157
- if (!component.props || !(key in component.props)) {
158
- attrs[key] = propsToPass[key];
159
- delete propsToPass[key];
160
- }
161
- }
162
- }
163
- }
164
-
165
- function resolveProps (route, config) {
166
- switch (typeof config) {
167
- case 'undefined':
168
- return
169
- case 'object':
170
- return config
171
- case 'function':
172
- return config(route)
173
- case 'boolean':
174
- return config ? route.params : undefined
175
- default:
176
- {
177
- warn(
178
- false,
179
- `props in "${route.path}" is a ${typeof config}, ` +
180
- `expecting an object, function or boolean.`
181
- );
182
- }
183
- }
184
- }
185
-
186
27
  /* */
187
28
 
188
29
  const encodeReserveRE = /[!'()*]/g;
@@ -192,11 +33,21 @@ const commaRE = /%2C/g;
192
33
  // fixed encodeURIComponent which is more conformant to RFC3986:
193
34
  // - escapes [!'()*]
194
35
  // - preserve commas
195
- const encode = str => encodeURIComponent(str)
196
- .replace(encodeReserveRE, encodeReserveReplacer)
197
- .replace(commaRE, ',');
36
+ const encode = str =>
37
+ encodeURIComponent(str)
38
+ .replace(encodeReserveRE, encodeReserveReplacer)
39
+ .replace(commaRE, ',');
198
40
 
199
- const decode = decodeURIComponent;
41
+ function decode (str) {
42
+ try {
43
+ return decodeURIComponent(str)
44
+ } catch (err) {
45
+ {
46
+ warn(false, `Error decoding "${str}". Leaving it intact.`);
47
+ }
48
+ }
49
+ return str
50
+ }
200
51
 
201
52
  function resolveQuery (
202
53
  query,
@@ -208,15 +59,20 @@ function resolveQuery (
208
59
  try {
209
60
  parsedQuery = parse(query || '');
210
61
  } catch (e) {
211
- warn(false, e.message);
62
+ warn(false, e.message);
212
63
  parsedQuery = {};
213
64
  }
214
65
  for (const key in extraQuery) {
215
- parsedQuery[key] = extraQuery[key];
66
+ const value = extraQuery[key];
67
+ parsedQuery[key] = Array.isArray(value)
68
+ ? value.map(castQueryParamValue)
69
+ : castQueryParamValue(value);
216
70
  }
217
71
  return parsedQuery
218
72
  }
219
73
 
74
+ const castQueryParamValue = value => (value == null || typeof value === 'object' ? value : String(value));
75
+
220
76
  function parseQuery (query) {
221
77
  const res = {};
222
78
 
@@ -229,9 +85,7 @@ function parseQuery (query) {
229
85
  query.split('&').forEach(param => {
230
86
  const parts = param.replace(/\+/g, ' ').split('=');
231
87
  const key = decode(parts.shift());
232
- const val = parts.length > 0
233
- ? decode(parts.join('='))
234
- : null;
88
+ const val = parts.length > 0 ? decode(parts.join('=')) : null;
235
89
 
236
90
  if (res[key] === undefined) {
237
91
  res[key] = val;
@@ -246,34 +100,39 @@ function parseQuery (query) {
246
100
  }
247
101
 
248
102
  function stringifyQuery (obj) {
249
- const res = obj ? Object.keys(obj).map(key => {
250
- const val = obj[key];
251
-
252
- if (val === undefined) {
253
- return ''
254
- }
103
+ const res = obj
104
+ ? Object.keys(obj)
105
+ .map(key => {
106
+ const val = obj[key];
255
107
 
256
- if (val === null) {
257
- return encode(key)
258
- }
108
+ if (val === undefined) {
109
+ return ''
110
+ }
259
111
 
260
- if (Array.isArray(val)) {
261
- const result = [];
262
- val.forEach(val2 => {
263
- if (val2 === undefined) {
264
- return
112
+ if (val === null) {
113
+ return encode(key)
265
114
  }
266
- if (val2 === null) {
267
- result.push(encode(key));
268
- } else {
269
- result.push(encode(key) + '=' + encode(val2));
115
+
116
+ if (Array.isArray(val)) {
117
+ const result = [];
118
+ val.forEach(val2 => {
119
+ if (val2 === undefined) {
120
+ return
121
+ }
122
+ if (val2 === null) {
123
+ result.push(encode(key));
124
+ } else {
125
+ result.push(encode(key) + '=' + encode(val2));
126
+ }
127
+ });
128
+ return result.join('&')
270
129
  }
271
- });
272
- return result.join('&')
273
- }
274
130
 
275
- return encode(key) + '=' + encode(val)
276
- }).filter(x => x.length > 0).join('&') : null;
131
+ return encode(key) + '=' + encode(val)
132
+ })
133
+ .filter(x => x.length > 0)
134
+ .join('&')
135
+ : null;
277
136
  return res ? `?${res}` : ''
278
137
  }
279
138
 
@@ -346,23 +205,23 @@ function getFullPath (
346
205
  return (path || '/') + stringify(query) + hash
347
206
  }
348
207
 
349
- function isSameRoute (a, b) {
208
+ function isSameRoute (a, b, onlyPath) {
350
209
  if (b === START) {
351
210
  return a === b
352
211
  } else if (!b) {
353
212
  return false
354
213
  } else if (a.path && b.path) {
355
- return (
356
- a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
214
+ return a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && (onlyPath ||
357
215
  a.hash === b.hash &&
358
- isObjectEqual(a.query, b.query)
359
- )
216
+ isObjectEqual(a.query, b.query))
360
217
  } else if (a.name && b.name) {
361
218
  return (
362
219
  a.name === b.name &&
363
- a.hash === b.hash &&
220
+ (onlyPath || (
221
+ a.hash === b.hash &&
364
222
  isObjectEqual(a.query, b.query) &&
365
- isObjectEqual(a.params, b.params)
223
+ isObjectEqual(a.params, b.params))
224
+ )
366
225
  )
367
226
  } else {
368
227
  return false
@@ -372,14 +231,18 @@ function isSameRoute (a, b) {
372
231
  function isObjectEqual (a = {}, b = {}) {
373
232
  // handle null value #1566
374
233
  if (!a || !b) return a === b
375
- const aKeys = Object.keys(a);
376
- const bKeys = Object.keys(b);
234
+ const aKeys = Object.keys(a).sort();
235
+ const bKeys = Object.keys(b).sort();
377
236
  if (aKeys.length !== bKeys.length) {
378
237
  return false
379
238
  }
380
- return aKeys.every(key => {
239
+ return aKeys.every((key, i) => {
381
240
  const aVal = a[key];
241
+ const bKey = bKeys[i];
242
+ if (bKey !== key) return false
382
243
  const bVal = b[key];
244
+ // query values can be null and undefined
245
+ if (aVal == null || bVal == null) return aVal === bVal
383
246
  // check nested equality
384
247
  if (typeof aVal === 'object' && typeof bVal === 'object') {
385
248
  return isObjectEqual(aVal, bVal)
@@ -407,6 +270,173 @@ function queryIncludes (current, target) {
407
270
  return true
408
271
  }
409
272
 
273
+ function handleRouteEntered (route) {
274
+ for (let i = 0; i < route.matched.length; i++) {
275
+ const record = route.matched[i];
276
+ for (const name in record.instances) {
277
+ const instance = record.instances[name];
278
+ const cbs = record.enteredCbs[name];
279
+ if (!instance || !cbs) continue
280
+ delete record.enteredCbs[name];
281
+ for (let i = 0; i < cbs.length; i++) {
282
+ if (!instance._isBeingDestroyed) cbs[i](instance);
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ var View = {
289
+ name: 'RouterView',
290
+ functional: true,
291
+ props: {
292
+ name: {
293
+ type: String,
294
+ default: 'default'
295
+ }
296
+ },
297
+ render (_, { props, children, parent, data }) {
298
+ // used by devtools to display a router-view badge
299
+ data.routerView = true;
300
+
301
+ // directly use parent context's createElement() function
302
+ // so that components rendered by router-view can resolve named slots
303
+ const h = parent.$createElement;
304
+ const name = props.name;
305
+ const route = parent.$route;
306
+ const cache = parent._routerViewCache || (parent._routerViewCache = {});
307
+
308
+ // determine current view depth, also check to see if the tree
309
+ // has been toggled inactive but kept-alive.
310
+ let depth = 0;
311
+ let inactive = false;
312
+ while (parent && parent._routerRoot !== parent) {
313
+ const knodeData = parent.$knode ? parent.$knode.data : {};
314
+ if (knodeData.routerView) {
315
+ depth++;
316
+ }
317
+ if (knodeData.keepAlive && parent._directInactive && parent._inactive) {
318
+ inactive = true;
319
+ }
320
+ parent = parent.$parent;
321
+ }
322
+ data.routerViewDepth = depth;
323
+
324
+ // render previous view if the tree is inactive and kept-alive
325
+ if (inactive) {
326
+ const cachedData = cache[name];
327
+ const cachedComponent = cachedData && cachedData.component;
328
+ if (cachedComponent) {
329
+ // #2301
330
+ // pass props
331
+ if (cachedData.configProps) {
332
+ fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps);
333
+ }
334
+ return h(cachedComponent, data, children)
335
+ } else {
336
+ // render previous empty view
337
+ return h()
338
+ }
339
+ }
340
+
341
+ const matched = route.matched[depth];
342
+ const component = matched && matched.components[name];
343
+
344
+ // render empty node if no matched route or no config component
345
+ if (!matched || !component) {
346
+ cache[name] = null;
347
+ return h()
348
+ }
349
+
350
+ // cache component
351
+ cache[name] = { component };
352
+
353
+ // attach instance registration hook
354
+ // this will be called in the instance's injected lifecycle hooks
355
+ data.registerRouteInstance = (vm, val) => {
356
+ // val could be undefined for unregistration
357
+ const current = matched.instances[name];
358
+ if (
359
+ (val && current !== vm) ||
360
+ (!val && current === vm)
361
+ ) {
362
+ matched.instances[name] = val;
363
+ }
364
+ }
365
+
366
+ // also register instance in prepatch hook
367
+ // in case the same component instance is reused across different routes
368
+ ;(data.hook || (data.hook = {})).prepatch = (_, knode) => {
369
+ matched.instances[name] = knode.componentInstance;
370
+ };
371
+
372
+ // register instance in init hook
373
+ // in case kept-alive component be actived when routes changed
374
+ data.hook.init = (knode) => {
375
+ if (knode.data.keepAlive &&
376
+ knode.componentInstance &&
377
+ knode.componentInstance !== matched.instances[name]
378
+ ) {
379
+ matched.instances[name] = knode.componentInstance;
380
+ }
381
+
382
+ // if the route transition has already been confirmed then we weren't
383
+ // able to call the cbs during confirmation as the component was not
384
+ // registered yet, so we call it here.
385
+ handleRouteEntered(route);
386
+ };
387
+
388
+ const configProps = matched.props && matched.props[name];
389
+ // save route and configProps in cache
390
+ if (configProps) {
391
+ extend(cache[name], {
392
+ route,
393
+ configProps
394
+ });
395
+ fillPropsinData(component, data, route, configProps);
396
+ }
397
+
398
+ return h(component, data, children)
399
+ }
400
+ };
401
+
402
+ function fillPropsinData (component, data, route, configProps) {
403
+ // resolve props
404
+ let propsToPass = data.props = resolveProps(route, configProps);
405
+ if (propsToPass) {
406
+ // clone to prevent mutation
407
+ propsToPass = data.props = extend({}, propsToPass);
408
+ // pass non-declared props as attrs
409
+ const attrs = data.attrs = data.attrs || {};
410
+ for (const key in propsToPass) {
411
+ if (!component.props || !(key in component.props)) {
412
+ attrs[key] = propsToPass[key];
413
+ delete propsToPass[key];
414
+ }
415
+ }
416
+ }
417
+ }
418
+
419
+ function resolveProps (route, config) {
420
+ switch (typeof config) {
421
+ case 'undefined':
422
+ return
423
+ case 'object':
424
+ return config
425
+ case 'function':
426
+ return config(route)
427
+ case 'boolean':
428
+ return config ? route.params : undefined
429
+ default:
430
+ {
431
+ warn(
432
+ false,
433
+ `props in "${route.path}" is a ${typeof config}, ` +
434
+ `expecting an object, function or boolean.`
435
+ );
436
+ }
437
+ }
438
+ }
439
+
410
440
  /* */
411
441
 
412
442
  function resolvePath (
@@ -475,7 +505,7 @@ function parsePath (path) {
475
505
  }
476
506
 
477
507
  function cleanPath (path) {
478
- return path.replace(/\/\//g, '/')
508
+ return path.replace(/\/(?:\s*\/)+/g, '/')
479
509
  }
480
510
 
481
511
  var isarray = Array.isArray || function (arr) {
@@ -1015,6 +1045,10 @@ const eventTypes = [String, Array];
1015
1045
 
1016
1046
  const noop = () => {};
1017
1047
 
1048
+ let warnedCustomSlot;
1049
+ let warnedTagProp;
1050
+ let warnedEventProp;
1051
+
1018
1052
  var Link = {
1019
1053
  name: 'RouterLink',
1020
1054
  props: {
@@ -1026,11 +1060,17 @@ var Link = {
1026
1060
  type: String,
1027
1061
  default: 'a'
1028
1062
  },
1063
+ custom: Boolean,
1029
1064
  exact: Boolean,
1065
+ exactPath: Boolean,
1030
1066
  append: Boolean,
1031
1067
  replace: Boolean,
1032
1068
  activeClass: String,
1033
1069
  exactActiveClass: String,
1070
+ ariaCurrentValue: {
1071
+ type: String,
1072
+ default: 'page'
1073
+ },
1034
1074
  event: {
1035
1075
  type: eventTypes,
1036
1076
  default: 'click'
@@ -1066,11 +1106,13 @@ var Link = {
1066
1106
  ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
1067
1107
  : route;
1068
1108
 
1069
- classes[exactActiveClass] = isSameRoute(current, compareTarget);
1070
- classes[activeClass] = this.exact
1109
+ classes[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath);
1110
+ classes[activeClass] = this.exact || this.exactPath
1071
1111
  ? classes[exactActiveClass]
1072
1112
  : isIncludedRoute(current, compareTarget);
1073
1113
 
1114
+ const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null;
1115
+
1074
1116
  const handler = e => {
1075
1117
  if (guardEvent(e)) {
1076
1118
  if (this.replace) {
@@ -1104,13 +1146,17 @@ var Link = {
1104
1146
  });
1105
1147
 
1106
1148
  if (scopedSlot) {
1149
+ if (!this.custom) {
1150
+ !warnedCustomSlot && warn(false, 'In Kdu Router 4, the k-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link k-slot="{ navigate, href }" custom></router-link>\n');
1151
+ warnedCustomSlot = true;
1152
+ }
1107
1153
  if (scopedSlot.length === 1) {
1108
1154
  return scopedSlot[0]
1109
1155
  } else if (scopedSlot.length > 1 || !scopedSlot.length) {
1110
1156
  {
1111
1157
  warn(
1112
1158
  false,
1113
- `RouterLink with to="${
1159
+ `<router-link> with to="${
1114
1160
  this.to
1115
1161
  }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
1116
1162
  );
@@ -1119,9 +1165,26 @@ var Link = {
1119
1165
  }
1120
1166
  }
1121
1167
 
1168
+ {
1169
+ if ('tag' in this.$options.propsData && !warnedTagProp) {
1170
+ warn(
1171
+ false,
1172
+ `<router-link>'s tag prop is deprecated and has been removed in Kdu Router 4. Use the k-slot API to remove this warning: https://kdujs-router.web.app/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
1173
+ );
1174
+ warnedTagProp = true;
1175
+ }
1176
+ if ('event' in this.$options.propsData && !warnedEventProp) {
1177
+ warn(
1178
+ false,
1179
+ `<router-link>'s event prop is deprecated and has been removed in Kdu Router 4. Use the k-slot API to remove this warning: https://kdujs-router.web.app/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
1180
+ );
1181
+ warnedEventProp = true;
1182
+ }
1183
+ }
1184
+
1122
1185
  if (this.tag === 'a') {
1123
1186
  data.on = on;
1124
- data.attrs = { href };
1187
+ data.attrs = { href, 'aria-current': ariaCurrentValue };
1125
1188
  } else {
1126
1189
  // find the first <a> child and apply listener and href
1127
1190
  const a = findAnchor(this.$slots.default);
@@ -1149,6 +1212,7 @@ var Link = {
1149
1212
 
1150
1213
  const aAttrs = (a.data.attrs = extend({}, a.data.attrs));
1151
1214
  aAttrs.href = href;
1215
+ aAttrs['aria-current'] = ariaCurrentValue;
1152
1216
  } else {
1153
1217
  // doesn't have <a> child, apply listener to self
1154
1218
  data.on = on;
@@ -1253,7 +1317,8 @@ function createRouteMap (
1253
1317
  routes,
1254
1318
  oldPathList,
1255
1319
  oldPathMap,
1256
- oldNameMap
1320
+ oldNameMap,
1321
+ parentRoute
1257
1322
  ) {
1258
1323
  // the path list is used to control path matching priority
1259
1324
  const pathList = oldPathList || [];
@@ -1263,7 +1328,7 @@ function createRouteMap (
1263
1328
  const nameMap = oldNameMap || Object.create(null);
1264
1329
 
1265
1330
  routes.forEach(route => {
1266
- addRouteRecord(pathList, pathMap, nameMap, route);
1331
+ addRouteRecord(pathList, pathMap, nameMap, route, parentRoute);
1267
1332
  });
1268
1333
 
1269
1334
  // ensure wildcard routes are always at the end
@@ -1311,6 +1376,14 @@ function addRouteRecord (
1311
1376
  path || name
1312
1377
  )} cannot be a ` + `string id. Use an actual component instead.`
1313
1378
  );
1379
+
1380
+ warn(
1381
+ // eslint-disable-next-line no-control-regex
1382
+ !/[^\u0000-\u007F]+/.test(path),
1383
+ `Route with path "${path}" contains unencoded characters, make sure ` +
1384
+ `your path is correctly encoded before passing it to the router. Use ` +
1385
+ `encodeURI to encode static segments of your path.`
1386
+ );
1314
1387
  }
1315
1388
 
1316
1389
  const pathToRegexpOptions =
@@ -1325,7 +1398,13 @@ function addRouteRecord (
1325
1398
  path: normalizedPath,
1326
1399
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
1327
1400
  components: route.components || { default: route.component },
1401
+ alias: route.alias
1402
+ ? typeof route.alias === 'string'
1403
+ ? [route.alias]
1404
+ : route.alias
1405
+ : [],
1328
1406
  instances: {},
1407
+ enteredCbs: {},
1329
1408
  name,
1330
1409
  parent,
1331
1410
  matchAs,
@@ -1355,7 +1434,7 @@ function addRouteRecord (
1355
1434
  `Named Route '${route.name}' has a default child route. ` +
1356
1435
  `When navigating to this named route (:to="{name: '${
1357
1436
  route.name
1358
- }'"), ` +
1437
+ }'}"), ` +
1359
1438
  `the default child route will not be rendered. Remove the name from ` +
1360
1439
  `this route and use the name of the default child route for named ` +
1361
1440
  `links instead.`
@@ -1379,7 +1458,7 @@ function addRouteRecord (
1379
1458
  const aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
1380
1459
  for (let i = 0; i < aliases.length; ++i) {
1381
1460
  const alias = aliases[i];
1382
- if ( alias === path) {
1461
+ if (alias === path) {
1383
1462
  warn(
1384
1463
  false,
1385
1464
  `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
@@ -1406,7 +1485,7 @@ function addRouteRecord (
1406
1485
  if (name) {
1407
1486
  if (!nameMap[name]) {
1408
1487
  nameMap[name] = record;
1409
- } else if ( !matchAs) {
1488
+ } else if (!matchAs) {
1410
1489
  warn(
1411
1490
  false,
1412
1491
  `Duplicate named routes definition: ` +
@@ -1459,6 +1538,28 @@ function createMatcher (
1459
1538
  createRouteMap(routes, pathList, pathMap, nameMap);
1460
1539
  }
1461
1540
 
1541
+ function addRoute (parentOrRoute, route) {
1542
+ const parent = (typeof parentOrRoute !== 'object') ? nameMap[parentOrRoute] : undefined;
1543
+ // $flow-disable-line
1544
+ createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent);
1545
+
1546
+ // add aliases of parent
1547
+ if (parent && parent.alias.length) {
1548
+ createRouteMap(
1549
+ // $flow-disable-line route is defined if parent is
1550
+ parent.alias.map(alias => ({ path: alias, children: [route] })),
1551
+ pathList,
1552
+ pathMap,
1553
+ nameMap,
1554
+ parent
1555
+ );
1556
+ }
1557
+ }
1558
+
1559
+ function getRoutes () {
1560
+ return pathList.map(path => pathMap[path])
1561
+ }
1562
+
1462
1563
  function match (
1463
1564
  raw,
1464
1565
  currentRoute,
@@ -1602,6 +1703,8 @@ function createMatcher (
1602
1703
 
1603
1704
  return {
1604
1705
  match,
1706
+ addRoute,
1707
+ getRoutes,
1605
1708
  addRoutes
1606
1709
  }
1607
1710
  }
@@ -1621,10 +1724,9 @@ function matchRoute (
1621
1724
 
1622
1725
  for (let i = 1, len = m.length; i < len; ++i) {
1623
1726
  const key = regex.keys[i - 1];
1624
- const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i];
1625
1727
  if (key) {
1626
1728
  // Fix #1994: using * with props: true generates a param named 0
1627
- params[key.name || 'pathMatch'] = val;
1729
+ params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i];
1628
1730
  }
1629
1731
  }
1630
1732
 
@@ -1662,6 +1764,10 @@ function setStateKey (key) {
1662
1764
  const positionStore = Object.create(null);
1663
1765
 
1664
1766
  function setupScroll () {
1767
+ // Prevent browser scroll behavior on History popstate
1768
+ if ('scrollRestoration' in window.history) {
1769
+ window.history.scrollRestoration = 'manual';
1770
+ }
1665
1771
  // Fix for #1585 for Firefox
1666
1772
  // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
1667
1773
  // Fix for #2774 Support for apps loaded from Windows file shares not mapped to network drives: replaced location.origin with
@@ -1673,12 +1779,10 @@ function setupScroll () {
1673
1779
  const stateCopy = extend({}, window.history.state);
1674
1780
  stateCopy.key = getStateKey();
1675
1781
  window.history.replaceState(stateCopy, '', absolutePath);
1676
- window.addEventListener('popstate', e => {
1677
- saveScrollPosition();
1678
- if (e.state && e.state.key) {
1679
- setStateKey(e.state.key);
1680
- }
1681
- });
1782
+ window.addEventListener('popstate', handlePopState);
1783
+ return () => {
1784
+ window.removeEventListener('popstate', handlePopState);
1785
+ }
1682
1786
  }
1683
1787
 
1684
1788
  function handleScroll (
@@ -1740,6 +1844,13 @@ function saveScrollPosition () {
1740
1844
  }
1741
1845
  }
1742
1846
 
1847
+ function handlePopState (e) {
1848
+ saveScrollPosition();
1849
+ if (e.state && e.state.key) {
1850
+ setStateKey(e.state.key);
1851
+ }
1852
+ }
1853
+
1743
1854
  function getScrollPosition () {
1744
1855
  const key = getStateKey();
1745
1856
  if (key) {
@@ -1805,7 +1916,17 @@ function scrollToPosition (shouldScroll, position) {
1805
1916
  }
1806
1917
 
1807
1918
  if (position) {
1808
- window.scrollTo(position.x, position.y);
1919
+ // $flow-disable-line
1920
+ if ('scrollBehavior' in document.documentElement.style) {
1921
+ window.scrollTo({
1922
+ left: position.x,
1923
+ top: position.y,
1924
+ // $flow-disable-line
1925
+ behavior: shouldScroll.behavior
1926
+ });
1927
+ } else {
1928
+ window.scrollTo(position.x, position.y);
1929
+ }
1809
1930
  }
1810
1931
  }
1811
1932
 
@@ -1825,7 +1946,7 @@ const supportsPushState =
1825
1946
  return false
1826
1947
  }
1827
1948
 
1828
- return window.history && 'pushState' in window.history
1949
+ return window.history && typeof window.history.pushState === 'function'
1829
1950
  })();
1830
1951
 
1831
1952
  function pushState (url, replace) {
@@ -1870,6 +1991,93 @@ function runQueue (queue, fn, cb) {
1870
1991
  step(0);
1871
1992
  }
1872
1993
 
1994
+ // When changing thing, also edit router.d.ts
1995
+ const NavigationFailureType = {
1996
+ redirected: 2,
1997
+ aborted: 4,
1998
+ cancelled: 8,
1999
+ duplicated: 16
2000
+ };
2001
+
2002
+ function createNavigationRedirectedError (from, to) {
2003
+ return createRouterError(
2004
+ from,
2005
+ to,
2006
+ NavigationFailureType.redirected,
2007
+ `Redirected when going from "${from.fullPath}" to "${stringifyRoute(
2008
+ to
2009
+ )}" via a navigation guard.`
2010
+ )
2011
+ }
2012
+
2013
+ function createNavigationDuplicatedError (from, to) {
2014
+ const error = createRouterError(
2015
+ from,
2016
+ to,
2017
+ NavigationFailureType.duplicated,
2018
+ `Avoided redundant navigation to current location: "${from.fullPath}".`
2019
+ );
2020
+ // backwards compatible with the first introduction of Errors
2021
+ error.name = 'NavigationDuplicated';
2022
+ return error
2023
+ }
2024
+
2025
+ function createNavigationCancelledError (from, to) {
2026
+ return createRouterError(
2027
+ from,
2028
+ to,
2029
+ NavigationFailureType.cancelled,
2030
+ `Navigation cancelled from "${from.fullPath}" to "${
2031
+ to.fullPath
2032
+ }" with a new navigation.`
2033
+ )
2034
+ }
2035
+
2036
+ function createNavigationAbortedError (from, to) {
2037
+ return createRouterError(
2038
+ from,
2039
+ to,
2040
+ NavigationFailureType.aborted,
2041
+ `Navigation aborted from "${from.fullPath}" to "${
2042
+ to.fullPath
2043
+ }" via a navigation guard.`
2044
+ )
2045
+ }
2046
+
2047
+ function createRouterError (from, to, type, message) {
2048
+ const error = new Error(message);
2049
+ error._isRouter = true;
2050
+ error.from = from;
2051
+ error.to = to;
2052
+ error.type = type;
2053
+
2054
+ return error
2055
+ }
2056
+
2057
+ const propertiesToLog = ['params', 'query', 'hash'];
2058
+
2059
+ function stringifyRoute (to) {
2060
+ if (typeof to === 'string') return to
2061
+ if ('path' in to) return to.path
2062
+ const location = {};
2063
+ propertiesToLog.forEach(key => {
2064
+ if (key in to) location[key] = to[key];
2065
+ });
2066
+ return JSON.stringify(location, null, 2)
2067
+ }
2068
+
2069
+ function isError (err) {
2070
+ return Object.prototype.toString.call(err).indexOf('Error') > -1
2071
+ }
2072
+
2073
+ function isNavigationFailure (err, errorType) {
2074
+ return (
2075
+ isError(err) &&
2076
+ err._isRouter &&
2077
+ (errorType == null || err.type === errorType)
2078
+ )
2079
+ }
2080
+
1873
2081
  /* */
1874
2082
 
1875
2083
  function resolveAsyncComponents (matched) {
@@ -1905,7 +2113,7 @@ function resolveAsyncComponents (matched) {
1905
2113
 
1906
2114
  const reject = once(reason => {
1907
2115
  const msg = `Failed to resolve async component ${key}: ${reason}`;
1908
- warn(false, msg);
2116
+ warn(false, msg);
1909
2117
  if (!error) {
1910
2118
  error = isError(reason)
1911
2119
  ? reason
@@ -1976,29 +2184,6 @@ function once (fn) {
1976
2184
  }
1977
2185
  }
1978
2186
 
1979
- class NavigationDuplicated extends Error {
1980
- constructor (normalizedLocation) {
1981
- super();
1982
- this.name = this._name = 'NavigationDuplicated';
1983
- // passing the message to super() doesn't seem to work in the transpiled version
1984
- this.message = `Navigating to current location ("${
1985
- normalizedLocation.fullPath
1986
- }") is not allowed`;
1987
- // add a stack property so services like Sentry can correctly display it
1988
- Object.defineProperty(this, 'stack', {
1989
- value: new Error().stack,
1990
- writable: true,
1991
- configurable: true
1992
- });
1993
- // we could also have used
1994
- // Error.captureStackTrace(this, this.constructor)
1995
- // but it only exists on node and chrome
1996
- }
1997
- }
1998
-
1999
- // support IE9
2000
- NavigationDuplicated._name = 'NavigationDuplicated';
2001
-
2002
2187
  /* */
2003
2188
 
2004
2189
  class History {
@@ -2011,6 +2196,8 @@ class History {
2011
2196
 
2012
2197
 
2013
2198
 
2199
+
2200
+
2014
2201
 
2015
2202
  // implemented by sub-classes
2016
2203
 
@@ -2018,6 +2205,7 @@ class History {
2018
2205
 
2019
2206
 
2020
2207
 
2208
+
2021
2209
 
2022
2210
  constructor (router, base) {
2023
2211
  this.router = router;
@@ -2029,6 +2217,7 @@ class History {
2029
2217
  this.readyCbs = [];
2030
2218
  this.readyErrorCbs = [];
2031
2219
  this.errorCbs = [];
2220
+ this.listeners = [];
2032
2221
  }
2033
2222
 
2034
2223
  listen (cb) {
@@ -2055,13 +2244,27 @@ class History {
2055
2244
  onComplete,
2056
2245
  onAbort
2057
2246
  ) {
2058
- const route = this.router.match(location, this.current);
2247
+ let route;
2248
+ // catch redirect option
2249
+ try {
2250
+ route = this.router.match(location, this.current);
2251
+ } catch (e) {
2252
+ this.errorCbs.forEach(cb => {
2253
+ cb(e);
2254
+ });
2255
+ // Exception should still be thrown
2256
+ throw e
2257
+ }
2258
+ const prev = this.current;
2059
2259
  this.confirmTransition(
2060
2260
  route,
2061
2261
  () => {
2062
2262
  this.updateRoute(route);
2063
2263
  onComplete && onComplete(route);
2064
2264
  this.ensureURL();
2265
+ this.router.afterHooks.forEach(hook => {
2266
+ hook && hook(route, prev);
2267
+ });
2065
2268
 
2066
2269
  // fire ready cbs once
2067
2270
  if (!this.ready) {
@@ -2076,10 +2279,14 @@ class History {
2076
2279
  onAbort(err);
2077
2280
  }
2078
2281
  if (err && !this.ready) {
2079
- this.ready = true;
2080
- this.readyErrorCbs.forEach(cb => {
2081
- cb(err);
2082
- });
2282
+ // Initial redirection should not mark the history as ready yet
2283
+ // because it's triggered by the redirection instead
2284
+ if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
2285
+ this.ready = true;
2286
+ this.readyErrorCbs.forEach(cb => {
2287
+ cb(err);
2288
+ });
2289
+ }
2083
2290
  }
2084
2291
  }
2085
2292
  );
@@ -2087,29 +2294,37 @@ class History {
2087
2294
 
2088
2295
  confirmTransition (route, onComplete, onAbort) {
2089
2296
  const current = this.current;
2297
+ this.pending = route;
2090
2298
  const abort = err => {
2091
- // When the user navigates through history through back/forward buttons
2092
- // we do not want to throw the error. We only throw it if directly calling
2093
- // push/replace. That's why it's not included in isError
2094
- if (!isExtendedError(NavigationDuplicated, err) && isError(err)) {
2299
+ // changed after adding errors
2300
+ // before that change, redirect and aborted navigation would produce an err == null
2301
+ if (!isNavigationFailure(err) && isError(err)) {
2095
2302
  if (this.errorCbs.length) {
2096
2303
  this.errorCbs.forEach(cb => {
2097
2304
  cb(err);
2098
2305
  });
2099
2306
  } else {
2100
- warn(false, 'uncaught error during route navigation:');
2307
+ {
2308
+ warn(false, 'uncaught error during route navigation:');
2309
+ }
2101
2310
  console.error(err);
2102
2311
  }
2103
2312
  }
2104
2313
  onAbort && onAbort(err);
2105
2314
  };
2315
+ const lastRouteIndex = route.matched.length - 1;
2316
+ const lastCurrentIndex = current.matched.length - 1;
2106
2317
  if (
2107
2318
  isSameRoute(route, current) &&
2108
2319
  // in the case the route map has been dynamically appended to
2109
- route.matched.length === current.matched.length
2320
+ lastRouteIndex === lastCurrentIndex &&
2321
+ route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
2110
2322
  ) {
2111
2323
  this.ensureURL();
2112
- return abort(new NavigationDuplicated(route))
2324
+ if (route.hash) {
2325
+ handleScroll(this.router, current, route, false);
2326
+ }
2327
+ return abort(createNavigationDuplicatedError(current, route))
2113
2328
  }
2114
2329
 
2115
2330
  const { updated, deactivated, activated } = resolveQueue(
@@ -2130,15 +2345,17 @@ class History {
2130
2345
  resolveAsyncComponents(activated)
2131
2346
  );
2132
2347
 
2133
- this.pending = route;
2134
2348
  const iterator = (hook, next) => {
2135
2349
  if (this.pending !== route) {
2136
- return abort()
2350
+ return abort(createNavigationCancelledError(current, route))
2137
2351
  }
2138
2352
  try {
2139
2353
  hook(route, current, (to) => {
2140
- if (to === false || isError(to)) {
2354
+ if (to === false) {
2141
2355
  // next(false) -> abort navigation, ensure current URL
2356
+ this.ensureURL(true);
2357
+ abort(createNavigationAbortedError(current, route));
2358
+ } else if (isError(to)) {
2142
2359
  this.ensureURL(true);
2143
2360
  abort(to);
2144
2361
  } else if (
@@ -2147,7 +2364,7 @@ class History {
2147
2364
  (typeof to.path === 'string' || typeof to.name === 'string'))
2148
2365
  ) {
2149
2366
  // next('/') or next({ path: '/' }) -> redirect
2150
- abort();
2367
+ abort(createNavigationRedirectedError(current, route));
2151
2368
  if (typeof to === 'object' && to.replace) {
2152
2369
  this.replace(to);
2153
2370
  } else {
@@ -2164,23 +2381,19 @@ class History {
2164
2381
  };
2165
2382
 
2166
2383
  runQueue(queue, iterator, () => {
2167
- const postEnterCbs = [];
2168
- const isValid = () => this.current === route;
2169
2384
  // wait until async components are resolved before
2170
2385
  // extracting in-component enter guards
2171
- const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
2386
+ const enterGuards = extractEnterGuards(activated);
2172
2387
  const queue = enterGuards.concat(this.router.resolveHooks);
2173
2388
  runQueue(queue, iterator, () => {
2174
2389
  if (this.pending !== route) {
2175
- return abort()
2390
+ return abort(createNavigationCancelledError(current, route))
2176
2391
  }
2177
2392
  this.pending = null;
2178
2393
  onComplete(route);
2179
2394
  if (this.router.app) {
2180
2395
  this.router.app.$nextTick(() => {
2181
- postEnterCbs.forEach(cb => {
2182
- cb();
2183
- });
2396
+ handleRouteEntered(route);
2184
2397
  });
2185
2398
  }
2186
2399
  });
@@ -2188,12 +2401,24 @@ class History {
2188
2401
  }
2189
2402
 
2190
2403
  updateRoute (route) {
2191
- const prev = this.current;
2192
2404
  this.current = route;
2193
2405
  this.cb && this.cb(route);
2194
- this.router.afterHooks.forEach(hook => {
2195
- hook && hook(route, prev);
2406
+ }
2407
+
2408
+ setupListeners () {
2409
+ // Default implementation is empty
2410
+ }
2411
+
2412
+ teardown () {
2413
+ // clean up event listeners
2414
+ this.listeners.forEach(cleanupListener => {
2415
+ cleanupListener();
2196
2416
  });
2417
+ this.listeners = [];
2418
+
2419
+ // reset current history route
2420
+ this.current = START;
2421
+ this.pending = null;
2197
2422
  }
2198
2423
  }
2199
2424
 
@@ -2280,15 +2505,13 @@ function bindGuard (guard, instance) {
2280
2505
  }
2281
2506
 
2282
2507
  function extractEnterGuards (
2283
- activated,
2284
- cbs,
2285
- isValid
2508
+ activated
2286
2509
  ) {
2287
2510
  return extractGuards(
2288
2511
  activated,
2289
2512
  'beforeRouteEnter',
2290
2513
  (guard, _, match, key) => {
2291
- return bindEnterGuard(guard, match, key, cbs, isValid)
2514
+ return bindEnterGuard(guard, match, key)
2292
2515
  }
2293
2516
  )
2294
2517
  }
@@ -2296,66 +2519,52 @@ function extractEnterGuards (
2296
2519
  function bindEnterGuard (
2297
2520
  guard,
2298
2521
  match,
2299
- key,
2300
- cbs,
2301
- isValid
2522
+ key
2302
2523
  ) {
2303
2524
  return function routeEnterGuard (to, from, next) {
2304
2525
  return guard(to, from, cb => {
2305
2526
  if (typeof cb === 'function') {
2306
- cbs.push(() => {
2307
- // #750
2308
- // if a router-view is wrapped with an out-in transition,
2309
- // the instance may not have been registered at this time.
2310
- // we will need to poll for registration until current route
2311
- // is no longer valid.
2312
- poll(cb, match.instances, key, isValid);
2313
- });
2527
+ if (!match.enteredCbs[key]) {
2528
+ match.enteredCbs[key] = [];
2529
+ }
2530
+ match.enteredCbs[key].push(cb);
2314
2531
  }
2315
2532
  next(cb);
2316
2533
  })
2317
2534
  }
2318
2535
  }
2319
2536
 
2320
- function poll (
2321
- cb, // somehow flow cannot infer this is a function
2322
- instances,
2323
- key,
2324
- isValid
2325
- ) {
2326
- if (
2327
- instances[key] &&
2328
- !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
2329
- ) {
2330
- cb(instances[key]);
2331
- } else if (isValid()) {
2332
- setTimeout(() => {
2333
- poll(cb, instances, key, isValid);
2334
- }, 16);
2335
- }
2336
- }
2337
-
2338
2537
  /* */
2339
2538
 
2340
2539
  class HTML5History extends History {
2540
+
2541
+
2341
2542
  constructor (router, base) {
2342
2543
  super(router, base);
2343
2544
 
2545
+ this._startLocation = getLocation(this.base);
2546
+ }
2547
+
2548
+ setupListeners () {
2549
+ if (this.listeners.length > 0) {
2550
+ return
2551
+ }
2552
+
2553
+ const router = this.router;
2344
2554
  const expectScroll = router.options.scrollBehavior;
2345
2555
  const supportsScroll = supportsPushState && expectScroll;
2346
2556
 
2347
2557
  if (supportsScroll) {
2348
- setupScroll();
2558
+ this.listeners.push(setupScroll());
2349
2559
  }
2350
2560
 
2351
- const initLocation = getLocation(this.base);
2352
- window.addEventListener('popstate', e => {
2561
+ const handleRoutingEvent = () => {
2353
2562
  const current = this.current;
2354
2563
 
2355
2564
  // Avoiding first `popstate` event dispatched in some browsers but first
2356
2565
  // history route not updated since async guard at the same time.
2357
2566
  const location = getLocation(this.base);
2358
- if (this.current === START && location === initLocation) {
2567
+ if (this.current === START && location === this._startLocation) {
2359
2568
  return
2360
2569
  }
2361
2570
 
@@ -2364,6 +2573,10 @@ class HTML5History extends History {
2364
2573
  handleScroll(router, route, current, true);
2365
2574
  }
2366
2575
  });
2576
+ };
2577
+ window.addEventListener('popstate', handleRoutingEvent);
2578
+ this.listeners.push(() => {
2579
+ window.removeEventListener('popstate', handleRoutingEvent);
2367
2580
  });
2368
2581
  }
2369
2582
 
@@ -2402,8 +2615,13 @@ class HTML5History extends History {
2402
2615
  }
2403
2616
 
2404
2617
  function getLocation (base) {
2405
- let path = decodeURI(window.location.pathname);
2406
- if (base && path.indexOf(base) === 0) {
2618
+ let path = window.location.pathname;
2619
+ const pathLowerCase = path.toLowerCase();
2620
+ const baseLowerCase = base.toLowerCase();
2621
+ // base="/a" shouldn't turn path="/app" into "/a/pp"
2622
+ // so we ensure the trailing slash in the base
2623
+ if (base && ((pathLowerCase === baseLowerCase) ||
2624
+ (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) {
2407
2625
  path = path.slice(base.length);
2408
2626
  }
2409
2627
  return (path || '/') + window.location.search + window.location.hash
@@ -2424,31 +2642,40 @@ class HashHistory extends History {
2424
2642
  // this is delayed until the app mounts
2425
2643
  // to avoid the hashchange listener being fired too early
2426
2644
  setupListeners () {
2645
+ if (this.listeners.length > 0) {
2646
+ return
2647
+ }
2648
+
2427
2649
  const router = this.router;
2428
2650
  const expectScroll = router.options.scrollBehavior;
2429
2651
  const supportsScroll = supportsPushState && expectScroll;
2430
2652
 
2431
2653
  if (supportsScroll) {
2432
- setupScroll();
2654
+ this.listeners.push(setupScroll());
2433
2655
  }
2434
2656
 
2435
- window.addEventListener(
2436
- supportsPushState ? 'popstate' : 'hashchange',
2437
- () => {
2438
- const current = this.current;
2439
- if (!ensureSlash()) {
2440
- return
2441
- }
2442
- this.transitionTo(getHash(), route => {
2443
- if (supportsScroll) {
2444
- handleScroll(this.router, route, current, true);
2445
- }
2446
- if (!supportsPushState) {
2447
- replaceHash(route.fullPath);
2448
- }
2449
- });
2657
+ const handleRoutingEvent = () => {
2658
+ const current = this.current;
2659
+ if (!ensureSlash()) {
2660
+ return
2450
2661
  }
2662
+ this.transitionTo(getHash(), route => {
2663
+ if (supportsScroll) {
2664
+ handleScroll(this.router, route, current, true);
2665
+ }
2666
+ if (!supportsPushState) {
2667
+ replaceHash(route.fullPath);
2668
+ }
2669
+ });
2670
+ };
2671
+ const eventType = supportsPushState ? 'popstate' : 'hashchange';
2672
+ window.addEventListener(
2673
+ eventType,
2674
+ handleRoutingEvent
2451
2675
  );
2676
+ this.listeners.push(() => {
2677
+ window.removeEventListener(eventType, handleRoutingEvent);
2678
+ });
2452
2679
  }
2453
2680
 
2454
2681
  push (location, onComplete, onAbort) {
@@ -2519,17 +2746,6 @@ function getHash () {
2519
2746
  if (index < 0) return ''
2520
2747
 
2521
2748
  href = href.slice(index + 1);
2522
- // decode the hash but not the search or hash
2523
- // as search(query) is already decoded
2524
- const searchIndex = href.indexOf('?');
2525
- if (searchIndex < 0) {
2526
- const hashIndex = href.indexOf('#');
2527
- if (hashIndex > -1) {
2528
- href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex);
2529
- } else href = decodeURI(href);
2530
- } else {
2531
- href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex);
2532
- }
2533
2749
 
2534
2750
  return href
2535
2751
  }
@@ -2601,11 +2817,15 @@ class AbstractHistory extends History {
2601
2817
  this.confirmTransition(
2602
2818
  route,
2603
2819
  () => {
2820
+ const prev = this.current;
2604
2821
  this.index = targetIndex;
2605
2822
  this.updateRoute(route);
2823
+ this.router.afterHooks.forEach(hook => {
2824
+ hook && hook(route, prev);
2825
+ });
2606
2826
  },
2607
2827
  err => {
2608
- if (isExtendedError(NavigationDuplicated, err)) {
2828
+ if (isNavigationFailure(err, NavigationFailureType.duplicated)) {
2609
2829
  this.index = targetIndex;
2610
2830
  }
2611
2831
  }
@@ -2624,11 +2844,12 @@ class AbstractHistory extends History {
2624
2844
 
2625
2845
  /* */
2626
2846
 
2627
-
2628
-
2629
2847
  class KduRouter {
2630
2848
 
2631
2849
 
2850
+
2851
+
2852
+
2632
2853
 
2633
2854
 
2634
2855
 
@@ -2644,6 +2865,9 @@ class KduRouter {
2644
2865
 
2645
2866
 
2646
2867
  constructor (options = {}) {
2868
+ {
2869
+ warn(this instanceof KduRouter, `Router must be called with the new operator.`);
2870
+ }
2647
2871
  this.app = null;
2648
2872
  this.apps = [];
2649
2873
  this.options = options;
@@ -2653,7 +2877,8 @@ class KduRouter {
2653
2877
  this.matcher = createMatcher(options.routes || [], this);
2654
2878
 
2655
2879
  let mode = options.mode || 'hash';
2656
- this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
2880
+ this.fallback =
2881
+ mode === 'history' && !supportsPushState && options.fallback !== false;
2657
2882
  if (this.fallback) {
2658
2883
  mode = 'hash';
2659
2884
  }
@@ -2679,11 +2904,7 @@ class KduRouter {
2679
2904
  }
2680
2905
  }
2681
2906
 
2682
- match (
2683
- raw,
2684
- current,
2685
- redirectedFrom
2686
- ) {
2907
+ match (raw, current, redirectedFrom) {
2687
2908
  return this.matcher.match(raw, current, redirectedFrom)
2688
2909
  }
2689
2910
 
@@ -2692,11 +2913,11 @@ class KduRouter {
2692
2913
  }
2693
2914
 
2694
2915
  init (app /* Kdu component instance */) {
2695
- assert(
2696
- install.installed,
2697
- `not installed. Make sure to call \`Kdu.use(KduRouter)\` ` +
2698
- `before creating root instance.`
2699
- );
2916
+ assert(
2917
+ install.installed,
2918
+ `not installed. Make sure to call \`Kdu.use(KduRouter)\` ` +
2919
+ `before creating root instance.`
2920
+ );
2700
2921
 
2701
2922
  this.apps.push(app);
2702
2923
 
@@ -2708,6 +2929,8 @@ class KduRouter {
2708
2929
  // ensure we still have a main app or null if no apps
2709
2930
  // we do not release the router so it can be reused
2710
2931
  if (this.app === app) this.app = this.apps[0] || null;
2932
+
2933
+ if (!this.app) this.history.teardown();
2711
2934
  });
2712
2935
 
2713
2936
  // main app previously initialized
@@ -2720,21 +2943,29 @@ class KduRouter {
2720
2943
 
2721
2944
  const history = this.history;
2722
2945
 
2723
- if (history instanceof HTML5History) {
2724
- history.transitionTo(history.getCurrentLocation());
2725
- } else if (history instanceof HashHistory) {
2726
- const setupHashListener = () => {
2946
+ if (history instanceof HTML5History || history instanceof HashHistory) {
2947
+ const handleInitialScroll = routeOrError => {
2948
+ const from = history.current;
2949
+ const expectScroll = this.options.scrollBehavior;
2950
+ const supportsScroll = supportsPushState && expectScroll;
2951
+
2952
+ if (supportsScroll && 'fullPath' in routeOrError) {
2953
+ handleScroll(this, routeOrError, from, false);
2954
+ }
2955
+ };
2956
+ const setupListeners = routeOrError => {
2727
2957
  history.setupListeners();
2958
+ handleInitialScroll(routeOrError);
2728
2959
  };
2729
2960
  history.transitionTo(
2730
2961
  history.getCurrentLocation(),
2731
- setupHashListener,
2732
- setupHashListener
2962
+ setupListeners,
2963
+ setupListeners
2733
2964
  );
2734
2965
  }
2735
2966
 
2736
2967
  history.listen(route => {
2737
- this.apps.forEach((app) => {
2968
+ this.apps.forEach(app => {
2738
2969
  app._route = route;
2739
2970
  });
2740
2971
  });
@@ -2803,11 +3034,14 @@ class KduRouter {
2803
3034
  if (!route) {
2804
3035
  return []
2805
3036
  }
2806
- return [].concat.apply([], route.matched.map(m => {
2807
- return Object.keys(m.components).map(key => {
2808
- return m.components[key]
3037
+ return [].concat.apply(
3038
+ [],
3039
+ route.matched.map(m => {
3040
+ return Object.keys(m.components).map(key => {
3041
+ return m.components[key]
3042
+ })
2809
3043
  })
2810
- }))
3044
+ )
2811
3045
  }
2812
3046
 
2813
3047
  resolve (
@@ -2816,12 +3050,7 @@ class KduRouter {
2816
3050
  append
2817
3051
  ) {
2818
3052
  current = current || this.history.current;
2819
- const location = normalizeLocation(
2820
- to,
2821
- current,
2822
- append,
2823
- this
2824
- );
3053
+ const location = normalizeLocation(to, current, append, this);
2825
3054
  const route = this.match(location, current);
2826
3055
  const fullPath = route.redirectedFrom || route.fullPath;
2827
3056
  const base = this.history.base;
@@ -2836,7 +3065,21 @@ class KduRouter {
2836
3065
  }
2837
3066
  }
2838
3067
 
3068
+ getRoutes () {
3069
+ return this.matcher.getRoutes()
3070
+ }
3071
+
3072
+ addRoute (parentOrRoute, route) {
3073
+ this.matcher.addRoute(parentOrRoute, route);
3074
+ if (this.history.current !== START) {
3075
+ this.history.transitionTo(this.history.getCurrentLocation());
3076
+ }
3077
+ }
3078
+
2839
3079
  addRoutes (routes) {
3080
+ {
3081
+ warn(false, 'router.addRoutes() is deprecated and has been removed in Kdu Router 4. Use router.addRoute() instead.');
3082
+ }
2840
3083
  this.matcher.addRoutes(routes);
2841
3084
  if (this.history.current !== START) {
2842
3085
  this.history.transitionTo(this.history.getCurrentLocation());
@@ -2858,7 +3101,10 @@ function createHref (base, fullPath, mode) {
2858
3101
  }
2859
3102
 
2860
3103
  KduRouter.install = install;
2861
- KduRouter.version = '3.1.7';
3104
+ KduRouter.version = '3.5.4';
3105
+ KduRouter.isNavigationFailure = isNavigationFailure;
3106
+ KduRouter.NavigationFailureType = NavigationFailureType;
3107
+ KduRouter.START_LOCATION = START;
2862
3108
 
2863
3109
  if (inBrowser && window.Kdu) {
2864
3110
  window.Kdu.use(KduRouter);