kdu-router 3.4.0-beta.0 → 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.4.0-beta.0
2
+ * kdu-router v3.5.4
3
3
  * (c) 2022 NKDuy
4
4
  * @license MIT
5
5
  */
@@ -12,7 +12,7 @@ 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
  }
@@ -24,153 +24,6 @@ function extend (a, b) {
24
24
  return a
25
25
  }
26
26
 
27
- var View = {
28
- name: 'RouterView',
29
- functional: true,
30
- props: {
31
- name: {
32
- type: String,
33
- default: 'default'
34
- }
35
- },
36
- render (_, { props, children, parent, data }) {
37
- // used by devtools to display a router-view badge
38
- data.routerView = true;
39
-
40
- // directly use parent context's createElement() function
41
- // so that components rendered by router-view can resolve named slots
42
- const h = parent.$createElement;
43
- const name = props.name;
44
- const route = parent.$route;
45
- const cache = parent._routerViewCache || (parent._routerViewCache = {});
46
-
47
- // determine current view depth, also check to see if the tree
48
- // has been toggled inactive but kept-alive.
49
- let depth = 0;
50
- let inactive = false;
51
- while (parent && parent._routerRoot !== parent) {
52
- const knodeData = parent.$knode ? parent.$knode.data : {};
53
- if (knodeData.routerView) {
54
- depth++;
55
- }
56
- if (knodeData.keepAlive && parent._directInactive && parent._inactive) {
57
- inactive = true;
58
- }
59
- parent = parent.$parent;
60
- }
61
- data.routerViewDepth = depth;
62
-
63
- // render previous view if the tree is inactive and kept-alive
64
- if (inactive) {
65
- const cachedData = cache[name];
66
- const cachedComponent = cachedData && cachedData.component;
67
- if (cachedComponent) {
68
- // #2301
69
- // pass props
70
- if (cachedData.configProps) {
71
- fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps);
72
- }
73
- return h(cachedComponent, data, children)
74
- } else {
75
- // render previous empty view
76
- return h()
77
- }
78
- }
79
-
80
- const matched = route.matched[depth];
81
- const component = matched && matched.components[name];
82
-
83
- // render empty node if no matched route or no config component
84
- if (!matched || !component) {
85
- cache[name] = null;
86
- return h()
87
- }
88
-
89
- // cache component
90
- cache[name] = { component };
91
-
92
- // attach instance registration hook
93
- // this will be called in the instance's injected lifecycle hooks
94
- data.registerRouteInstance = (vm, val) => {
95
- // val could be undefined for unregistration
96
- const current = matched.instances[name];
97
- if (
98
- (val && current !== vm) ||
99
- (!val && current === vm)
100
- ) {
101
- matched.instances[name] = val;
102
- }
103
- }
104
-
105
- // also register instance in prepatch hook
106
- // in case the same component instance is reused across different routes
107
- ;(data.hook || (data.hook = {})).prepatch = (_, knode) => {
108
- matched.instances[name] = knode.componentInstance;
109
- };
110
-
111
- // register instance in init hook
112
- // in case kept-alive component be actived when routes changed
113
- data.hook.init = (knode) => {
114
- if (knode.data.keepAlive &&
115
- knode.componentInstance &&
116
- knode.componentInstance !== matched.instances[name]
117
- ) {
118
- matched.instances[name] = knode.componentInstance;
119
- }
120
- };
121
-
122
- const configProps = matched.props && matched.props[name];
123
- // save route and configProps in cache
124
- if (configProps) {
125
- extend(cache[name], {
126
- route,
127
- configProps
128
- });
129
- fillPropsinData(component, data, route, configProps);
130
- }
131
-
132
- return h(component, data, children)
133
- }
134
- };
135
-
136
- function fillPropsinData (component, data, route, configProps) {
137
- // resolve props
138
- let propsToPass = data.props = resolveProps(route, configProps);
139
- if (propsToPass) {
140
- // clone to prevent mutation
141
- propsToPass = data.props = extend({}, propsToPass);
142
- // pass non-declared props as attrs
143
- const attrs = data.attrs = data.attrs || {};
144
- for (const key in propsToPass) {
145
- if (!component.props || !(key in component.props)) {
146
- attrs[key] = propsToPass[key];
147
- delete propsToPass[key];
148
- }
149
- }
150
- }
151
- }
152
-
153
- function resolveProps (route, config) {
154
- switch (typeof config) {
155
- case 'undefined':
156
- return
157
- case 'object':
158
- return config
159
- case 'function':
160
- return config(route)
161
- case 'boolean':
162
- return config ? route.params : undefined
163
- default:
164
- {
165
- warn(
166
- false,
167
- `props in "${route.path}" is a ${typeof config}, ` +
168
- `expecting an object, function or boolean.`
169
- );
170
- }
171
- }
172
- }
173
-
174
27
  /* */
175
28
 
176
29
  const encodeReserveRE = /[!'()*]/g;
@@ -180,11 +33,21 @@ const commaRE = /%2C/g;
180
33
  // fixed encodeURIComponent which is more conformant to RFC3986:
181
34
  // - escapes [!'()*]
182
35
  // - preserve commas
183
- const encode = str => encodeURIComponent(str)
184
- .replace(encodeReserveRE, encodeReserveReplacer)
185
- .replace(commaRE, ',');
36
+ const encode = str =>
37
+ encodeURIComponent(str)
38
+ .replace(encodeReserveRE, encodeReserveReplacer)
39
+ .replace(commaRE, ',');
186
40
 
187
- 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
+ }
188
51
 
189
52
  function resolveQuery (
190
53
  query,
@@ -196,16 +59,20 @@ function resolveQuery (
196
59
  try {
197
60
  parsedQuery = parse(query || '');
198
61
  } catch (e) {
199
- warn(false, e.message);
62
+ warn(false, e.message);
200
63
  parsedQuery = {};
201
64
  }
202
65
  for (const key in extraQuery) {
203
66
  const value = extraQuery[key];
204
- parsedQuery[key] = Array.isArray(value) ? value.map(v => '' + v) : '' + value;
67
+ parsedQuery[key] = Array.isArray(value)
68
+ ? value.map(castQueryParamValue)
69
+ : castQueryParamValue(value);
205
70
  }
206
71
  return parsedQuery
207
72
  }
208
73
 
74
+ const castQueryParamValue = value => (value == null || typeof value === 'object' ? value : String(value));
75
+
209
76
  function parseQuery (query) {
210
77
  const res = {};
211
78
 
@@ -218,9 +85,7 @@ function parseQuery (query) {
218
85
  query.split('&').forEach(param => {
219
86
  const parts = param.replace(/\+/g, ' ').split('=');
220
87
  const key = decode(parts.shift());
221
- const val = parts.length > 0
222
- ? decode(parts.join('='))
223
- : null;
88
+ const val = parts.length > 0 ? decode(parts.join('=')) : null;
224
89
 
225
90
  if (res[key] === undefined) {
226
91
  res[key] = val;
@@ -235,34 +100,39 @@ function parseQuery (query) {
235
100
  }
236
101
 
237
102
  function stringifyQuery (obj) {
238
- const res = obj ? Object.keys(obj).map(key => {
239
- const val = obj[key];
103
+ const res = obj
104
+ ? Object.keys(obj)
105
+ .map(key => {
106
+ const val = obj[key];
240
107
 
241
- if (val === undefined) {
242
- return ''
243
- }
244
-
245
- if (val === null) {
246
- return encode(key)
247
- }
108
+ if (val === undefined) {
109
+ return ''
110
+ }
248
111
 
249
- if (Array.isArray(val)) {
250
- const result = [];
251
- val.forEach(val2 => {
252
- if (val2 === undefined) {
253
- return
112
+ if (val === null) {
113
+ return encode(key)
254
114
  }
255
- if (val2 === null) {
256
- result.push(encode(key));
257
- } else {
258
- 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('&')
259
129
  }
260
- });
261
- return result.join('&')
262
- }
263
130
 
264
- return encode(key) + '=' + encode(val)
265
- }).filter(x => x.length > 0).join('&') : null;
131
+ return encode(key) + '=' + encode(val)
132
+ })
133
+ .filter(x => x.length > 0)
134
+ .join('&')
135
+ : null;
266
136
  return res ? `?${res}` : ''
267
137
  }
268
138
 
@@ -335,23 +205,23 @@ function getFullPath (
335
205
  return (path || '/') + stringify(query) + hash
336
206
  }
337
207
 
338
- function isSameRoute (a, b) {
208
+ function isSameRoute (a, b, onlyPath) {
339
209
  if (b === START) {
340
210
  return a === b
341
211
  } else if (!b) {
342
212
  return false
343
213
  } else if (a.path && b.path) {
344
- return (
345
- a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
214
+ return a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') && (onlyPath ||
346
215
  a.hash === b.hash &&
347
- isObjectEqual(a.query, b.query)
348
- )
216
+ isObjectEqual(a.query, b.query))
349
217
  } else if (a.name && b.name) {
350
218
  return (
351
219
  a.name === b.name &&
352
- a.hash === b.hash &&
220
+ (onlyPath || (
221
+ a.hash === b.hash &&
353
222
  isObjectEqual(a.query, b.query) &&
354
- isObjectEqual(a.params, b.params)
223
+ isObjectEqual(a.params, b.params))
224
+ )
355
225
  )
356
226
  } else {
357
227
  return false
@@ -361,14 +231,18 @@ function isSameRoute (a, b) {
361
231
  function isObjectEqual (a = {}, b = {}) {
362
232
  // handle null value #1566
363
233
  if (!a || !b) return a === b
364
- const aKeys = Object.keys(a);
365
- const bKeys = Object.keys(b);
234
+ const aKeys = Object.keys(a).sort();
235
+ const bKeys = Object.keys(b).sort();
366
236
  if (aKeys.length !== bKeys.length) {
367
237
  return false
368
238
  }
369
- return aKeys.every(key => {
239
+ return aKeys.every((key, i) => {
370
240
  const aVal = a[key];
241
+ const bKey = bKeys[i];
242
+ if (bKey !== key) return false
371
243
  const bVal = b[key];
244
+ // query values can be null and undefined
245
+ if (aVal == null || bVal == null) return aVal === bVal
372
246
  // check nested equality
373
247
  if (typeof aVal === 'object' && typeof bVal === 'object') {
374
248
  return isObjectEqual(aVal, bVal)
@@ -396,6 +270,173 @@ function queryIncludes (current, target) {
396
270
  return true
397
271
  }
398
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
+
399
440
  /* */
400
441
 
401
442
  function resolvePath (
@@ -464,7 +505,7 @@ function parsePath (path) {
464
505
  }
465
506
 
466
507
  function cleanPath (path) {
467
- return path.replace(/\/\//g, '/')
508
+ return path.replace(/\/(?:\s*\/)+/g, '/')
468
509
  }
469
510
 
470
511
  var isarray = Array.isArray || function (arr) {
@@ -1004,6 +1045,10 @@ const eventTypes = [String, Array];
1004
1045
 
1005
1046
  const noop = () => {};
1006
1047
 
1048
+ let warnedCustomSlot;
1049
+ let warnedTagProp;
1050
+ let warnedEventProp;
1051
+
1007
1052
  var Link = {
1008
1053
  name: 'RouterLink',
1009
1054
  props: {
@@ -1015,7 +1060,9 @@ var Link = {
1015
1060
  type: String,
1016
1061
  default: 'a'
1017
1062
  },
1063
+ custom: Boolean,
1018
1064
  exact: Boolean,
1065
+ exactPath: Boolean,
1019
1066
  append: Boolean,
1020
1067
  replace: Boolean,
1021
1068
  activeClass: String,
@@ -1059,8 +1106,8 @@ var Link = {
1059
1106
  ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
1060
1107
  : route;
1061
1108
 
1062
- classes[exactActiveClass] = isSameRoute(current, compareTarget);
1063
- classes[activeClass] = this.exact
1109
+ classes[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath);
1110
+ classes[activeClass] = this.exact || this.exactPath
1064
1111
  ? classes[exactActiveClass]
1065
1112
  : isIncludedRoute(current, compareTarget);
1066
1113
 
@@ -1099,13 +1146,17 @@ var Link = {
1099
1146
  });
1100
1147
 
1101
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
+ }
1102
1153
  if (scopedSlot.length === 1) {
1103
1154
  return scopedSlot[0]
1104
1155
  } else if (scopedSlot.length > 1 || !scopedSlot.length) {
1105
1156
  {
1106
1157
  warn(
1107
1158
  false,
1108
- `RouterLink with to="${
1159
+ `<router-link> with to="${
1109
1160
  this.to
1110
1161
  }" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
1111
1162
  );
@@ -1114,6 +1165,23 @@ var Link = {
1114
1165
  }
1115
1166
  }
1116
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
+
1117
1185
  if (this.tag === 'a') {
1118
1186
  data.on = on;
1119
1187
  data.attrs = { href, 'aria-current': ariaCurrentValue };
@@ -1249,7 +1317,8 @@ function createRouteMap (
1249
1317
  routes,
1250
1318
  oldPathList,
1251
1319
  oldPathMap,
1252
- oldNameMap
1320
+ oldNameMap,
1321
+ parentRoute
1253
1322
  ) {
1254
1323
  // the path list is used to control path matching priority
1255
1324
  const pathList = oldPathList || [];
@@ -1259,7 +1328,7 @@ function createRouteMap (
1259
1328
  const nameMap = oldNameMap || Object.create(null);
1260
1329
 
1261
1330
  routes.forEach(route => {
1262
- addRouteRecord(pathList, pathMap, nameMap, route);
1331
+ addRouteRecord(pathList, pathMap, nameMap, route, parentRoute);
1263
1332
  });
1264
1333
 
1265
1334
  // ensure wildcard routes are always at the end
@@ -1307,6 +1376,14 @@ function addRouteRecord (
1307
1376
  path || name
1308
1377
  )} cannot be a ` + `string id. Use an actual component instead.`
1309
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
+ );
1310
1387
  }
1311
1388
 
1312
1389
  const pathToRegexpOptions =
@@ -1321,7 +1398,13 @@ function addRouteRecord (
1321
1398
  path: normalizedPath,
1322
1399
  regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
1323
1400
  components: route.components || { default: route.component },
1401
+ alias: route.alias
1402
+ ? typeof route.alias === 'string'
1403
+ ? [route.alias]
1404
+ : route.alias
1405
+ : [],
1324
1406
  instances: {},
1407
+ enteredCbs: {},
1325
1408
  name,
1326
1409
  parent,
1327
1410
  matchAs,
@@ -1351,7 +1434,7 @@ function addRouteRecord (
1351
1434
  `Named Route '${route.name}' has a default child route. ` +
1352
1435
  `When navigating to this named route (:to="{name: '${
1353
1436
  route.name
1354
- }'"), ` +
1437
+ }'}"), ` +
1355
1438
  `the default child route will not be rendered. Remove the name from ` +
1356
1439
  `this route and use the name of the default child route for named ` +
1357
1440
  `links instead.`
@@ -1375,7 +1458,7 @@ function addRouteRecord (
1375
1458
  const aliases = Array.isArray(route.alias) ? route.alias : [route.alias];
1376
1459
  for (let i = 0; i < aliases.length; ++i) {
1377
1460
  const alias = aliases[i];
1378
- if ( alias === path) {
1461
+ if (alias === path) {
1379
1462
  warn(
1380
1463
  false,
1381
1464
  `Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
@@ -1402,7 +1485,7 @@ function addRouteRecord (
1402
1485
  if (name) {
1403
1486
  if (!nameMap[name]) {
1404
1487
  nameMap[name] = record;
1405
- } else if ( !matchAs) {
1488
+ } else if (!matchAs) {
1406
1489
  warn(
1407
1490
  false,
1408
1491
  `Duplicate named routes definition: ` +
@@ -1455,6 +1538,28 @@ function createMatcher (
1455
1538
  createRouteMap(routes, pathList, pathMap, nameMap);
1456
1539
  }
1457
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
+
1458
1563
  function match (
1459
1564
  raw,
1460
1565
  currentRoute,
@@ -1598,6 +1703,8 @@ function createMatcher (
1598
1703
 
1599
1704
  return {
1600
1705
  match,
1706
+ addRoute,
1707
+ getRoutes,
1601
1708
  addRoutes
1602
1709
  }
1603
1710
  }
@@ -1617,10 +1724,9 @@ function matchRoute (
1617
1724
 
1618
1725
  for (let i = 1, len = m.length; i < len; ++i) {
1619
1726
  const key = regex.keys[i - 1];
1620
- const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i];
1621
1727
  if (key) {
1622
1728
  // Fix #1994: using * with props: true generates a param named 0
1623
- params[key.name || 'pathMatch'] = val;
1729
+ params[key.name || 'pathMatch'] = typeof m[i] === 'string' ? decode(m[i]) : m[i];
1624
1730
  }
1625
1731
  }
1626
1732
 
@@ -1810,7 +1916,17 @@ function scrollToPosition (shouldScroll, position) {
1810
1916
  }
1811
1917
 
1812
1918
  if (position) {
1813
- 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
+ }
1814
1930
  }
1815
1931
  }
1816
1932
 
@@ -1875,6 +1991,7 @@ function runQueue (queue, fn, cb) {
1875
1991
  step(0);
1876
1992
  }
1877
1993
 
1994
+ // When changing thing, also edit router.d.ts
1878
1995
  const NavigationFailureType = {
1879
1996
  redirected: 2,
1880
1997
  aborted: 4,
@@ -1996,7 +2113,7 @@ function resolveAsyncComponents (matched) {
1996
2113
 
1997
2114
  const reject = once(reason => {
1998
2115
  const msg = `Failed to resolve async component ${key}: ${reason}`;
1999
- warn(false, msg);
2116
+ warn(false, msg);
2000
2117
  if (!error) {
2001
2118
  error = isError(reason)
2002
2119
  ? reason
@@ -2128,6 +2245,7 @@ class History {
2128
2245
  onAbort
2129
2246
  ) {
2130
2247
  let route;
2248
+ // catch redirect option
2131
2249
  try {
2132
2250
  route = this.router.match(location, this.current);
2133
2251
  } catch (e) {
@@ -2137,10 +2255,10 @@ class History {
2137
2255
  // Exception should still be thrown
2138
2256
  throw e
2139
2257
  }
2258
+ const prev = this.current;
2140
2259
  this.confirmTransition(
2141
2260
  route,
2142
2261
  () => {
2143
- const prev = this.current;
2144
2262
  this.updateRoute(route);
2145
2263
  onComplete && onComplete(route);
2146
2264
  this.ensureURL();
@@ -2161,16 +2279,13 @@ class History {
2161
2279
  onAbort(err);
2162
2280
  }
2163
2281
  if (err && !this.ready) {
2164
- this.ready = true;
2165
- // Initial redirection should still trigger the onReady onSuccess
2166
- if (!isNavigationFailure(err, NavigationFailureType.redirected)) {
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;
2167
2286
  this.readyErrorCbs.forEach(cb => {
2168
2287
  cb(err);
2169
2288
  });
2170
- } else {
2171
- this.readyCbs.forEach(cb => {
2172
- cb(route);
2173
- });
2174
2289
  }
2175
2290
  }
2176
2291
  }
@@ -2179,16 +2294,19 @@ class History {
2179
2294
 
2180
2295
  confirmTransition (route, onComplete, onAbort) {
2181
2296
  const current = this.current;
2297
+ this.pending = route;
2182
2298
  const abort = err => {
2183
- // changed after adding errors before that change,
2184
- // redirect and aborted navigation would produce an err == null
2299
+ // changed after adding errors
2300
+ // before that change, redirect and aborted navigation would produce an err == null
2185
2301
  if (!isNavigationFailure(err) && isError(err)) {
2186
2302
  if (this.errorCbs.length) {
2187
2303
  this.errorCbs.forEach(cb => {
2188
2304
  cb(err);
2189
2305
  });
2190
2306
  } else {
2191
- warn(false, 'uncaught error during route navigation:');
2307
+ {
2308
+ warn(false, 'uncaught error during route navigation:');
2309
+ }
2192
2310
  console.error(err);
2193
2311
  }
2194
2312
  }
@@ -2203,6 +2321,9 @@ class History {
2203
2321
  route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
2204
2322
  ) {
2205
2323
  this.ensureURL();
2324
+ if (route.hash) {
2325
+ handleScroll(this.router, current, route, false);
2326
+ }
2206
2327
  return abort(createNavigationDuplicatedError(current, route))
2207
2328
  }
2208
2329
 
@@ -2224,7 +2345,6 @@ class History {
2224
2345
  resolveAsyncComponents(activated)
2225
2346
  );
2226
2347
 
2227
- this.pending = route;
2228
2348
  const iterator = (hook, next) => {
2229
2349
  if (this.pending !== route) {
2230
2350
  return abort(createNavigationCancelledError(current, route))
@@ -2261,11 +2381,9 @@ class History {
2261
2381
  };
2262
2382
 
2263
2383
  runQueue(queue, iterator, () => {
2264
- const postEnterCbs = [];
2265
- const isValid = () => this.current === route;
2266
2384
  // wait until async components are resolved before
2267
2385
  // extracting in-component enter guards
2268
- const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
2386
+ const enterGuards = extractEnterGuards(activated);
2269
2387
  const queue = enterGuards.concat(this.router.resolveHooks);
2270
2388
  runQueue(queue, iterator, () => {
2271
2389
  if (this.pending !== route) {
@@ -2275,9 +2393,7 @@ class History {
2275
2393
  onComplete(route);
2276
2394
  if (this.router.app) {
2277
2395
  this.router.app.$nextTick(() => {
2278
- postEnterCbs.forEach(cb => {
2279
- cb();
2280
- });
2396
+ handleRouteEntered(route);
2281
2397
  });
2282
2398
  }
2283
2399
  });
@@ -2293,11 +2409,16 @@ class History {
2293
2409
  // Default implementation is empty
2294
2410
  }
2295
2411
 
2296
- teardownListeners () {
2412
+ teardown () {
2413
+ // clean up event listeners
2297
2414
  this.listeners.forEach(cleanupListener => {
2298
2415
  cleanupListener();
2299
2416
  });
2300
2417
  this.listeners = [];
2418
+
2419
+ // reset current history route
2420
+ this.current = START;
2421
+ this.pending = null;
2301
2422
  }
2302
2423
  }
2303
2424
 
@@ -2384,15 +2505,13 @@ function bindGuard (guard, instance) {
2384
2505
  }
2385
2506
 
2386
2507
  function extractEnterGuards (
2387
- activated,
2388
- cbs,
2389
- isValid
2508
+ activated
2390
2509
  ) {
2391
2510
  return extractGuards(
2392
2511
  activated,
2393
2512
  'beforeRouteEnter',
2394
2513
  (guard, _, match, key) => {
2395
- return bindEnterGuard(guard, match, key, cbs, isValid)
2514
+ return bindEnterGuard(guard, match, key)
2396
2515
  }
2397
2516
  )
2398
2517
  }
@@ -2400,45 +2519,21 @@ function extractEnterGuards (
2400
2519
  function bindEnterGuard (
2401
2520
  guard,
2402
2521
  match,
2403
- key,
2404
- cbs,
2405
- isValid
2522
+ key
2406
2523
  ) {
2407
2524
  return function routeEnterGuard (to, from, next) {
2408
2525
  return guard(to, from, cb => {
2409
2526
  if (typeof cb === 'function') {
2410
- cbs.push(() => {
2411
- // #750
2412
- // if a router-view is wrapped with an out-in transition,
2413
- // the instance may not have been registered at this time.
2414
- // we will need to poll for registration until current route
2415
- // is no longer valid.
2416
- poll(cb, match.instances, key, isValid);
2417
- });
2527
+ if (!match.enteredCbs[key]) {
2528
+ match.enteredCbs[key] = [];
2529
+ }
2530
+ match.enteredCbs[key].push(cb);
2418
2531
  }
2419
2532
  next(cb);
2420
2533
  })
2421
2534
  }
2422
2535
  }
2423
2536
 
2424
- function poll (
2425
- cb, // somehow flow cannot infer this is a function
2426
- instances,
2427
- key,
2428
- isValid
2429
- ) {
2430
- if (
2431
- instances[key] &&
2432
- !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
2433
- ) {
2434
- cb(instances[key]);
2435
- } else if (isValid()) {
2436
- setTimeout(() => {
2437
- poll(cb, instances, key, isValid);
2438
- }, 16);
2439
- }
2440
- }
2441
-
2442
2537
  /* */
2443
2538
 
2444
2539
  class HTML5History extends History {
@@ -2520,8 +2615,13 @@ class HTML5History extends History {
2520
2615
  }
2521
2616
 
2522
2617
  function getLocation (base) {
2523
- let path = decodeURI(window.location.pathname);
2524
- if (base && path.toLowerCase().indexOf(base.toLowerCase()) === 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))) {
2525
2625
  path = path.slice(base.length);
2526
2626
  }
2527
2627
  return (path || '/') + window.location.search + window.location.hash
@@ -2646,17 +2746,6 @@ function getHash () {
2646
2746
  if (index < 0) return ''
2647
2747
 
2648
2748
  href = href.slice(index + 1);
2649
- // decode the hash but not the search or hash
2650
- // as search(query) is already decoded
2651
- const searchIndex = href.indexOf('?');
2652
- if (searchIndex < 0) {
2653
- const hashIndex = href.indexOf('#');
2654
- if (hashIndex > -1) {
2655
- href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex);
2656
- } else href = decodeURI(href);
2657
- } else {
2658
- href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex);
2659
- }
2660
2749
 
2661
2750
  return href
2662
2751
  }
@@ -2728,8 +2817,12 @@ class AbstractHistory extends History {
2728
2817
  this.confirmTransition(
2729
2818
  route,
2730
2819
  () => {
2820
+ const prev = this.current;
2731
2821
  this.index = targetIndex;
2732
2822
  this.updateRoute(route);
2823
+ this.router.afterHooks.forEach(hook => {
2824
+ hook && hook(route, prev);
2825
+ });
2733
2826
  },
2734
2827
  err => {
2735
2828
  if (isNavigationFailure(err, NavigationFailureType.duplicated)) {
@@ -2756,6 +2849,7 @@ class KduRouter {
2756
2849
 
2757
2850
 
2758
2851
 
2852
+
2759
2853
 
2760
2854
 
2761
2855
 
@@ -2771,6 +2865,9 @@ class KduRouter {
2771
2865
 
2772
2866
 
2773
2867
  constructor (options = {}) {
2868
+ {
2869
+ warn(this instanceof KduRouter, `Router must be called with the new operator.`);
2870
+ }
2774
2871
  this.app = null;
2775
2872
  this.apps = [];
2776
2873
  this.options = options;
@@ -2816,8 +2913,7 @@ class KduRouter {
2816
2913
  }
2817
2914
 
2818
2915
  init (app /* Kdu component instance */) {
2819
-
2820
- assert(
2916
+ assert(
2821
2917
  install.installed,
2822
2918
  `not installed. Make sure to call \`Kdu.use(KduRouter)\` ` +
2823
2919
  `before creating root instance.`
@@ -2834,10 +2930,7 @@ class KduRouter {
2834
2930
  // we do not release the router so it can be reused
2835
2931
  if (this.app === app) this.app = this.apps[0] || null;
2836
2932
 
2837
- if (!this.app) {
2838
- // clean up event listeners
2839
- this.history.teardownListeners();
2840
- }
2933
+ if (!this.app) this.history.teardown();
2841
2934
  });
2842
2935
 
2843
2936
  // main app previously initialized
@@ -2972,7 +3065,21 @@ class KduRouter {
2972
3065
  }
2973
3066
  }
2974
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
+
2975
3079
  addRoutes (routes) {
3080
+ {
3081
+ warn(false, 'router.addRoutes() is deprecated and has been removed in Kdu Router 4. Use router.addRoute() instead.');
3082
+ }
2976
3083
  this.matcher.addRoutes(routes);
2977
3084
  if (this.history.current !== START) {
2978
3085
  this.history.transitionTo(this.history.getCurrentLocation());
@@ -2994,9 +3101,10 @@ function createHref (base, fullPath, mode) {
2994
3101
  }
2995
3102
 
2996
3103
  KduRouter.install = install;
2997
- KduRouter.version = '3.4.0-beta.0';
3104
+ KduRouter.version = '3.5.4';
2998
3105
  KduRouter.isNavigationFailure = isNavigationFailure;
2999
3106
  KduRouter.NavigationFailureType = NavigationFailureType;
3107
+ KduRouter.START_LOCATION = START;
3000
3108
 
3001
3109
  if (inBrowser && window.Kdu) {
3002
3110
  window.Kdu.use(KduRouter);