UNPKG

63 kBJavaScriptView Raw
1/*!
2 * vue-router v3.0.4
3 * (c) 2019 Evan You
4 * @license MIT
5 */
6/* */
7
8function assert (condition, message) {
9 if (!condition) {
10 throw new Error(`[vue-router] ${message}`)
11 }
12}
13
14function warn (condition, message) {
15 if ("development" !== 'production' && !condition) {
16 typeof console !== 'undefined' && console.warn(`[vue-router] ${message}`);
17 }
18}
19
20function isError (err) {
21 return Object.prototype.toString.call(err).indexOf('Error') > -1
22}
23
24function extend (a, b) {
25 for (const key in b) {
26 a[key] = b[key];
27 }
28 return a
29}
30
31var View = {
32 name: 'RouterView',
33 functional: true,
34 props: {
35 name: {
36 type: String,
37 default: 'default'
38 }
39 },
40 render (_, { props, children, parent, data }) {
41 // used by devtools to display a router-view badge
42 data.routerView = true;
43
44 // directly use parent context's createElement() function
45 // so that components rendered by router-view can resolve named slots
46 const h = parent.$createElement;
47 const name = props.name;
48 const route = parent.$route;
49 const cache = parent._routerViewCache || (parent._routerViewCache = {});
50
51 // determine current view depth, also check to see if the tree
52 // has been toggled inactive but kept-alive.
53 let depth = 0;
54 let inactive = false;
55 while (parent && parent._routerRoot !== parent) {
56 if (parent.$vnode && parent.$vnode.data.routerView) {
57 depth++;
58 }
59 if (parent._inactive) {
60 inactive = true;
61 }
62 parent = parent.$parent;
63 }
64 data.routerViewDepth = depth;
65
66 // render previous view if the tree is inactive and kept-alive
67 if (inactive) {
68 return h(cache[name], data, children)
69 }
70
71 const matched = route.matched[depth];
72 // render empty node if no matched route
73 if (!matched) {
74 cache[name] = null;
75 return h()
76 }
77
78 const component = cache[name] = matched.components[name];
79
80 // attach instance registration hook
81 // this will be called in the instance's injected lifecycle hooks
82 data.registerRouteInstance = (vm, val) => {
83 // val could be undefined for unregistration
84 const current = matched.instances[name];
85 if (
86 (val && current !== vm) ||
87 (!val && current === vm)
88 ) {
89 matched.instances[name] = val;
90 }
91 }
92
93 // also register instance in prepatch hook
94 // in case the same component instance is reused across different routes
95 ;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
96 matched.instances[name] = vnode.componentInstance;
97 };
98
99 // resolve props
100 let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name]);
101 if (propsToPass) {
102 // clone to prevent mutation
103 propsToPass = data.props = extend({}, propsToPass);
104 // pass non-declared props as attrs
105 const attrs = data.attrs = data.attrs || {};
106 for (const key in propsToPass) {
107 if (!component.props || !(key in component.props)) {
108 attrs[key] = propsToPass[key];
109 delete propsToPass[key];
110 }
111 }
112 }
113
114 return h(component, data, children)
115 }
116}
117
118function resolveProps (route, config) {
119 switch (typeof config) {
120 case 'undefined':
121 return
122 case 'object':
123 return config
124 case 'function':
125 return config(route)
126 case 'boolean':
127 return config ? route.params : undefined
128 default:
129 {
130 warn(
131 false,
132 `props in "${route.path}" is a ${typeof config}, ` +
133 `expecting an object, function or boolean.`
134 );
135 }
136 }
137}
138
139/* */
140
141const encodeReserveRE = /[!'()*]/g;
142const encodeReserveReplacer = c => '%' + c.charCodeAt(0).toString(16);
143const commaRE = /%2C/g;
144
145// fixed encodeURIComponent which is more conformant to RFC3986:
146// - escapes [!'()*]
147// - preserve commas
148const encode = str => encodeURIComponent(str)
149 .replace(encodeReserveRE, encodeReserveReplacer)
150 .replace(commaRE, ',');
151
152const decode = decodeURIComponent;
153
154function resolveQuery (
155 query,
156 extraQuery = {},
157 _parseQuery
158) {
159 const parse = _parseQuery || parseQuery;
160 let parsedQuery;
161 try {
162 parsedQuery = parse(query || '');
163 } catch (e) {
164 "development" !== 'production' && warn(false, e.message);
165 parsedQuery = {};
166 }
167 for (const key in extraQuery) {
168 parsedQuery[key] = extraQuery[key];
169 }
170 return parsedQuery
171}
172
173function parseQuery (query) {
174 const res = {};
175
176 query = query.trim().replace(/^(\?|#|&)/, '');
177
178 if (!query) {
179 return res
180 }
181
182 query.split('&').forEach(param => {
183 const parts = param.replace(/\+/g, ' ').split('=');
184 const key = decode(parts.shift());
185 const val = parts.length > 0
186 ? decode(parts.join('='))
187 : null;
188
189 if (res[key] === undefined) {
190 res[key] = val;
191 } else if (Array.isArray(res[key])) {
192 res[key].push(val);
193 } else {
194 res[key] = [res[key], val];
195 }
196 });
197
198 return res
199}
200
201function stringifyQuery (obj) {
202 const res = obj ? Object.keys(obj).map(key => {
203 const val = obj[key];
204
205 if (val === undefined) {
206 return ''
207 }
208
209 if (val === null) {
210 return encode(key)
211 }
212
213 if (Array.isArray(val)) {
214 const result = [];
215 val.forEach(val2 => {
216 if (val2 === undefined) {
217 return
218 }
219 if (val2 === null) {
220 result.push(encode(key));
221 } else {
222 result.push(encode(key) + '=' + encode(val2));
223 }
224 });
225 return result.join('&')
226 }
227
228 return encode(key) + '=' + encode(val)
229 }).filter(x => x.length > 0).join('&') : null;
230 return res ? `?${res}` : ''
231}
232
233/* */
234
235const trailingSlashRE = /\/?$/;
236
237function createRoute (
238 record,
239 location,
240 redirectedFrom,
241 router
242) {
243 const stringifyQuery$$1 = router && router.options.stringifyQuery;
244
245 let query = location.query || {};
246 try {
247 query = clone(query);
248 } catch (e) {}
249
250 const route = {
251 name: location.name || (record && record.name),
252 meta: (record && record.meta) || {},
253 path: location.path || '/',
254 hash: location.hash || '',
255 query,
256 params: location.params || {},
257 fullPath: getFullPath(location, stringifyQuery$$1),
258 matched: record ? formatMatch(record) : []
259 };
260 if (redirectedFrom) {
261 route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery$$1);
262 }
263 return Object.freeze(route)
264}
265
266function clone (value) {
267 if (Array.isArray(value)) {
268 return value.map(clone)
269 } else if (value && typeof value === 'object') {
270 const res = {};
271 for (const key in value) {
272 res[key] = clone(value[key]);
273 }
274 return res
275 } else {
276 return value
277 }
278}
279
280// the starting route that represents the initial state
281const START = createRoute(null, {
282 path: '/'
283});
284
285function formatMatch (record) {
286 const res = [];
287 while (record) {
288 res.unshift(record);
289 record = record.parent;
290 }
291 return res
292}
293
294function getFullPath (
295 { path, query = {}, hash = '' },
296 _stringifyQuery
297) {
298 const stringify = _stringifyQuery || stringifyQuery;
299 return (path || '/') + stringify(query) + hash
300}
301
302function isSameRoute (a, b) {
303 if (b === START) {
304 return a === b
305 } else if (!b) {
306 return false
307 } else if (a.path && b.path) {
308 return (
309 a.path.replace(trailingSlashRE, '') === b.path.replace(trailingSlashRE, '') &&
310 a.hash === b.hash &&
311 isObjectEqual(a.query, b.query)
312 )
313 } else if (a.name && b.name) {
314 return (
315 a.name === b.name &&
316 a.hash === b.hash &&
317 isObjectEqual(a.query, b.query) &&
318 isObjectEqual(a.params, b.params)
319 )
320 } else {
321 return false
322 }
323}
324
325function isObjectEqual (a = {}, b = {}) {
326 // handle null value #1566
327 if (!a || !b) return a === b
328 const aKeys = Object.keys(a);
329 const bKeys = Object.keys(b);
330 if (aKeys.length !== bKeys.length) {
331 return false
332 }
333 return aKeys.every(key => {
334 const aVal = a[key];
335 const bVal = b[key];
336 // check nested equality
337 if (typeof aVal === 'object' && typeof bVal === 'object') {
338 return isObjectEqual(aVal, bVal)
339 }
340 return String(aVal) === String(bVal)
341 })
342}
343
344function isIncludedRoute (current, target) {
345 return (
346 current.path.replace(trailingSlashRE, '/').indexOf(
347 target.path.replace(trailingSlashRE, '/')
348 ) === 0 &&
349 (!target.hash || current.hash === target.hash) &&
350 queryIncludes(current.query, target.query)
351 )
352}
353
354function queryIncludes (current, target) {
355 for (const key in target) {
356 if (!(key in current)) {
357 return false
358 }
359 }
360 return true
361}
362
363/* */
364
365// work around weird flow bug
366const toTypes = [String, Object];
367const eventTypes = [String, Array];
368
369var Link = {
370 name: 'RouterLink',
371 props: {
372 to: {
373 type: toTypes,
374 required: true
375 },
376 tag: {
377 type: String,
378 default: 'a'
379 },
380 exact: Boolean,
381 append: Boolean,
382 replace: Boolean,
383 activeClass: String,
384 exactActiveClass: String,
385 event: {
386 type: eventTypes,
387 default: 'click'
388 }
389 },
390 render (h) {
391 const router = this.$router;
392 const current = this.$route;
393 const { location, route, href } = router.resolve(this.to, current, this.append);
394
395 const classes = {};
396 const globalActiveClass = router.options.linkActiveClass;
397 const globalExactActiveClass = router.options.linkExactActiveClass;
398 // Support global empty active class
399 const activeClassFallback = globalActiveClass == null
400 ? 'router-link-active'
401 : globalActiveClass;
402 const exactActiveClassFallback = globalExactActiveClass == null
403 ? 'router-link-exact-active'
404 : globalExactActiveClass;
405 const activeClass = this.activeClass == null
406 ? activeClassFallback
407 : this.activeClass;
408 const exactActiveClass = this.exactActiveClass == null
409 ? exactActiveClassFallback
410 : this.exactActiveClass;
411 const compareTarget = location.path
412 ? createRoute(null, location, null, router)
413 : route;
414
415 classes[exactActiveClass] = isSameRoute(current, compareTarget);
416 classes[activeClass] = this.exact
417 ? classes[exactActiveClass]
418 : isIncludedRoute(current, compareTarget);
419
420 const handler = e => {
421 if (guardEvent(e)) {
422 if (this.replace) {
423 router.replace(location);
424 } else {
425 router.push(location);
426 }
427 }
428 };
429
430 const on = { click: guardEvent };
431 if (Array.isArray(this.event)) {
432 this.event.forEach(e => { on[e] = handler; });
433 } else {
434 on[this.event] = handler;
435 }
436
437 const data = {
438 class: classes
439 };
440
441 if (this.tag === 'a') {
442 data.on = on;
443 data.attrs = { href };
444 } else {
445 // find the first <a> child and apply listener and href
446 const a = findAnchor(this.$slots.default);
447 if (a) {
448 // in case the <a> is a static node
449 a.isStatic = false;
450 const aData = a.data = extend({}, a.data);
451 aData.on = on;
452 const aAttrs = a.data.attrs = extend({}, a.data.attrs);
453 aAttrs.href = href;
454 } else {
455 // doesn't have <a> child, apply listener to self
456 data.on = on;
457 }
458 }
459
460 return h(this.tag, data, this.$slots.default)
461 }
462}
463
464function guardEvent (e) {
465 // don't redirect with control keys
466 if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
467 // don't redirect when preventDefault called
468 if (e.defaultPrevented) return
469 // don't redirect on right click
470 if (e.button !== undefined && e.button !== 0) return
471 // don't redirect if `target="_blank"`
472 if (e.currentTarget && e.currentTarget.getAttribute) {
473 const target = e.currentTarget.getAttribute('target');
474 if (/\b_blank\b/i.test(target)) return
475 }
476 // this may be a Weex event which doesn't have this method
477 if (e.preventDefault) {
478 e.preventDefault();
479 }
480 return true
481}
482
483function findAnchor (children) {
484 if (children) {
485 let child;
486 for (let i = 0; i < children.length; i++) {
487 child = children[i];
488 if (child.tag === 'a') {
489 return child
490 }
491 if (child.children && (child = findAnchor(child.children))) {
492 return child
493 }
494 }
495 }
496}
497
498let _Vue;
499
500function install (Vue) {
501 if (install.installed && _Vue === Vue) return
502 install.installed = true;
503
504 _Vue = Vue;
505
506 const isDef = v => v !== undefined;
507
508 const registerInstance = (vm, callVal) => {
509 let i = vm.$options._parentVnode;
510 if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
511 i(vm, callVal);
512 }
513 };
514
515 Vue.mixin({
516 beforeCreate () {
517 if (isDef(this.$options.router)) {
518 this._routerRoot = this;
519 this._router = this.$options.router;
520 this._router.init(this);
521 Vue.util.defineReactive(this, '_route', this._router.history.current);
522 } else {
523 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
524 }
525 registerInstance(this, this);
526 },
527 destroyed () {
528 registerInstance(this);
529 }
530 });
531
532 Object.defineProperty(Vue.prototype, '$router', {
533 get () { return this._routerRoot._router }
534 });
535
536 Object.defineProperty(Vue.prototype, '$route', {
537 get () { return this._routerRoot._route }
538 });
539
540 Vue.component('RouterView', View);
541 Vue.component('RouterLink', Link);
542
543 const strats = Vue.config.optionMergeStrategies;
544 // use the same hook merging strategy for route hooks
545 strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created;
546}
547
548/* */
549
550const inBrowser = typeof window !== 'undefined';
551
552/* */
553
554function resolvePath (
555 relative,
556 base,
557 append
558) {
559 const firstChar = relative.charAt(0);
560 if (firstChar === '/') {
561 return relative
562 }
563
564 if (firstChar === '?' || firstChar === '#') {
565 return base + relative
566 }
567
568 const stack = base.split('/');
569
570 // remove trailing segment if:
571 // - not appending
572 // - appending to trailing slash (last segment is empty)
573 if (!append || !stack[stack.length - 1]) {
574 stack.pop();
575 }
576
577 // resolve relative path
578 const segments = relative.replace(/^\//, '').split('/');
579 for (let i = 0; i < segments.length; i++) {
580 const segment = segments[i];
581 if (segment === '..') {
582 stack.pop();
583 } else if (segment !== '.') {
584 stack.push(segment);
585 }
586 }
587
588 // ensure leading slash
589 if (stack[0] !== '') {
590 stack.unshift('');
591 }
592
593 return stack.join('/')
594}
595
596function parsePath (path) {
597 let hash = '';
598 let query = '';
599
600 const hashIndex = path.indexOf('#');
601 if (hashIndex >= 0) {
602 hash = path.slice(hashIndex);
603 path = path.slice(0, hashIndex);
604 }
605
606 const queryIndex = path.indexOf('?');
607 if (queryIndex >= 0) {
608 query = path.slice(queryIndex + 1);
609 path = path.slice(0, queryIndex);
610 }
611
612 return {
613 path,
614 query,
615 hash
616 }
617}
618
619function cleanPath (path) {
620 return path.replace(/\/\//g, '/')
621}
622
623var isarray = Array.isArray || function (arr) {
624 return Object.prototype.toString.call(arr) == '[object Array]';
625};
626
627/**
628 * Expose `pathToRegexp`.
629 */
630var pathToRegexp_1 = pathToRegexp;
631var parse_1 = parse;
632var compile_1 = compile;
633var tokensToFunction_1 = tokensToFunction;
634var tokensToRegExp_1 = tokensToRegExp;
635
636/**
637 * The main path matching regexp utility.
638 *
639 * @type {RegExp}
640 */
641var PATH_REGEXP = new RegExp([
642 // Match escaped characters that would otherwise appear in future matches.
643 // This allows the user to escape special characters that won't transform.
644 '(\\\\.)',
645 // Match Express-style parameters and un-named parameters with a prefix
646 // and optional suffixes. Matches appear as:
647 //
648 // "/:test(\\d+)?" => ["/", "test", "\d+", undefined, "?", undefined]
649 // "/route(\\d+)" => [undefined, undefined, undefined, "\d+", undefined, undefined]
650 // "/*" => ["/", undefined, undefined, undefined, undefined, "*"]
651 '([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))'
652].join('|'), 'g');
653
654/**
655 * Parse a string for the raw tokens.
656 *
657 * @param {string} str
658 * @param {Object=} options
659 * @return {!Array}
660 */
661function parse (str, options) {
662 var tokens = [];
663 var key = 0;
664 var index = 0;
665 var path = '';
666 var defaultDelimiter = options && options.delimiter || '/';
667 var res;
668
669 while ((res = PATH_REGEXP.exec(str)) != null) {
670 var m = res[0];
671 var escaped = res[1];
672 var offset = res.index;
673 path += str.slice(index, offset);
674 index = offset + m.length;
675
676 // Ignore already escaped sequences.
677 if (escaped) {
678 path += escaped[1];
679 continue
680 }
681
682 var next = str[index];
683 var prefix = res[2];
684 var name = res[3];
685 var capture = res[4];
686 var group = res[5];
687 var modifier = res[6];
688 var asterisk = res[7];
689
690 // Push the current path onto the tokens.
691 if (path) {
692 tokens.push(path);
693 path = '';
694 }
695
696 var partial = prefix != null && next != null && next !== prefix;
697 var repeat = modifier === '+' || modifier === '*';
698 var optional = modifier === '?' || modifier === '*';
699 var delimiter = res[2] || defaultDelimiter;
700 var pattern = capture || group;
701
702 tokens.push({
703 name: name || key++,
704 prefix: prefix || '',
705 delimiter: delimiter,
706 optional: optional,
707 repeat: repeat,
708 partial: partial,
709 asterisk: !!asterisk,
710 pattern: pattern ? escapeGroup(pattern) : (asterisk ? '.*' : '[^' + escapeString(delimiter) + ']+?')
711 });
712 }
713
714 // Match any characters still remaining.
715 if (index < str.length) {
716 path += str.substr(index);
717 }
718
719 // If the path exists, push it onto the end.
720 if (path) {
721 tokens.push(path);
722 }
723
724 return tokens
725}
726
727/**
728 * Compile a string to a template function for the path.
729 *
730 * @param {string} str
731 * @param {Object=} options
732 * @return {!function(Object=, Object=)}
733 */
734function compile (str, options) {
735 return tokensToFunction(parse(str, options))
736}
737
738/**
739 * Prettier encoding of URI path segments.
740 *
741 * @param {string}
742 * @return {string}
743 */
744function encodeURIComponentPretty (str) {
745 return encodeURI(str).replace(/[\/?#]/g, function (c) {
746 return '%' + c.charCodeAt(0).toString(16).toUpperCase()
747 })
748}
749
750/**
751 * Encode the asterisk parameter. Similar to `pretty`, but allows slashes.
752 *
753 * @param {string}
754 * @return {string}
755 */
756function encodeAsterisk (str) {
757 return encodeURI(str).replace(/[?#]/g, function (c) {
758 return '%' + c.charCodeAt(0).toString(16).toUpperCase()
759 })
760}
761
762/**
763 * Expose a method for transforming tokens into the path function.
764 */
765function tokensToFunction (tokens) {
766 // Compile all the tokens into regexps.
767 var matches = new Array(tokens.length);
768
769 // Compile all the patterns before compilation.
770 for (var i = 0; i < tokens.length; i++) {
771 if (typeof tokens[i] === 'object') {
772 matches[i] = new RegExp('^(?:' + tokens[i].pattern + ')$');
773 }
774 }
775
776 return function (obj, opts) {
777 var path = '';
778 var data = obj || {};
779 var options = opts || {};
780 var encode = options.pretty ? encodeURIComponentPretty : encodeURIComponent;
781
782 for (var i = 0; i < tokens.length; i++) {
783 var token = tokens[i];
784
785 if (typeof token === 'string') {
786 path += token;
787
788 continue
789 }
790
791 var value = data[token.name];
792 var segment;
793
794 if (value == null) {
795 if (token.optional) {
796 // Prepend partial segment prefixes.
797 if (token.partial) {
798 path += token.prefix;
799 }
800
801 continue
802 } else {
803 throw new TypeError('Expected "' + token.name + '" to be defined')
804 }
805 }
806
807 if (isarray(value)) {
808 if (!token.repeat) {
809 throw new TypeError('Expected "' + token.name + '" to not repeat, but received `' + JSON.stringify(value) + '`')
810 }
811
812 if (value.length === 0) {
813 if (token.optional) {
814 continue
815 } else {
816 throw new TypeError('Expected "' + token.name + '" to not be empty')
817 }
818 }
819
820 for (var j = 0; j < value.length; j++) {
821 segment = encode(value[j]);
822
823 if (!matches[i].test(segment)) {
824 throw new TypeError('Expected all "' + token.name + '" to match "' + token.pattern + '", but received `' + JSON.stringify(segment) + '`')
825 }
826
827 path += (j === 0 ? token.prefix : token.delimiter) + segment;
828 }
829
830 continue
831 }
832
833 segment = token.asterisk ? encodeAsterisk(value) : encode(value);
834
835 if (!matches[i].test(segment)) {
836 throw new TypeError('Expected "' + token.name + '" to match "' + token.pattern + '", but received "' + segment + '"')
837 }
838
839 path += token.prefix + segment;
840 }
841
842 return path
843 }
844}
845
846/**
847 * Escape a regular expression string.
848 *
849 * @param {string} str
850 * @return {string}
851 */
852function escapeString (str) {
853 return str.replace(/([.+*?=^!:${}()[\]|\/\\])/g, '\\$1')
854}
855
856/**
857 * Escape the capturing group by escaping special characters and meaning.
858 *
859 * @param {string} group
860 * @return {string}
861 */
862function escapeGroup (group) {
863 return group.replace(/([=!:$\/()])/g, '\\$1')
864}
865
866/**
867 * Attach the keys as a property of the regexp.
868 *
869 * @param {!RegExp} re
870 * @param {Array} keys
871 * @return {!RegExp}
872 */
873function attachKeys (re, keys) {
874 re.keys = keys;
875 return re
876}
877
878/**
879 * Get the flags for a regexp from the options.
880 *
881 * @param {Object} options
882 * @return {string}
883 */
884function flags (options) {
885 return options.sensitive ? '' : 'i'
886}
887
888/**
889 * Pull out keys from a regexp.
890 *
891 * @param {!RegExp} path
892 * @param {!Array} keys
893 * @return {!RegExp}
894 */
895function regexpToRegexp (path, keys) {
896 // Use a negative lookahead to match only capturing groups.
897 var groups = path.source.match(/\((?!\?)/g);
898
899 if (groups) {
900 for (var i = 0; i < groups.length; i++) {
901 keys.push({
902 name: i,
903 prefix: null,
904 delimiter: null,
905 optional: false,
906 repeat: false,
907 partial: false,
908 asterisk: false,
909 pattern: null
910 });
911 }
912 }
913
914 return attachKeys(path, keys)
915}
916
917/**
918 * Transform an array into a regexp.
919 *
920 * @param {!Array} path
921 * @param {Array} keys
922 * @param {!Object} options
923 * @return {!RegExp}
924 */
925function arrayToRegexp (path, keys, options) {
926 var parts = [];
927
928 for (var i = 0; i < path.length; i++) {
929 parts.push(pathToRegexp(path[i], keys, options).source);
930 }
931
932 var regexp = new RegExp('(?:' + parts.join('|') + ')', flags(options));
933
934 return attachKeys(regexp, keys)
935}
936
937/**
938 * Create a path regexp from string input.
939 *
940 * @param {string} path
941 * @param {!Array} keys
942 * @param {!Object} options
943 * @return {!RegExp}
944 */
945function stringToRegexp (path, keys, options) {
946 return tokensToRegExp(parse(path, options), keys, options)
947}
948
949/**
950 * Expose a function for taking tokens and returning a RegExp.
951 *
952 * @param {!Array} tokens
953 * @param {(Array|Object)=} keys
954 * @param {Object=} options
955 * @return {!RegExp}
956 */
957function tokensToRegExp (tokens, keys, options) {
958 if (!isarray(keys)) {
959 options = /** @type {!Object} */ (keys || options);
960 keys = [];
961 }
962
963 options = options || {};
964
965 var strict = options.strict;
966 var end = options.end !== false;
967 var route = '';
968
969 // Iterate over the tokens and create our regexp string.
970 for (var i = 0; i < tokens.length; i++) {
971 var token = tokens[i];
972
973 if (typeof token === 'string') {
974 route += escapeString(token);
975 } else {
976 var prefix = escapeString(token.prefix);
977 var capture = '(?:' + token.pattern + ')';
978
979 keys.push(token);
980
981 if (token.repeat) {
982 capture += '(?:' + prefix + capture + ')*';
983 }
984
985 if (token.optional) {
986 if (!token.partial) {
987 capture = '(?:' + prefix + '(' + capture + '))?';
988 } else {
989 capture = prefix + '(' + capture + ')?';
990 }
991 } else {
992 capture = prefix + '(' + capture + ')';
993 }
994
995 route += capture;
996 }
997 }
998
999 var delimiter = escapeString(options.delimiter || '/');
1000 var endsWithDelimiter = route.slice(-delimiter.length) === delimiter;
1001
1002 // In non-strict mode we allow a slash at the end of match. If the path to
1003 // match already ends with a slash, we remove it for consistency. The slash
1004 // is valid at the end of a path match, not in the middle. This is important
1005 // in non-ending mode, where "/test/" shouldn't match "/test//route".
1006 if (!strict) {
1007 route = (endsWithDelimiter ? route.slice(0, -delimiter.length) : route) + '(?:' + delimiter + '(?=$))?';
1008 }
1009
1010 if (end) {
1011 route += '$';
1012 } else {
1013 // In non-ending mode, we need the capturing groups to match as much as
1014 // possible by using a positive lookahead to the end or next path segment.
1015 route += strict && endsWithDelimiter ? '' : '(?=' + delimiter + '|$)';
1016 }
1017
1018 return attachKeys(new RegExp('^' + route, flags(options)), keys)
1019}
1020
1021/**
1022 * Normalize the given path string, returning a regular expression.
1023 *
1024 * An empty array can be passed in for the keys, which will hold the
1025 * placeholder key descriptions. For example, using `/user/:id`, `keys` will
1026 * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
1027 *
1028 * @param {(string|RegExp|Array)} path
1029 * @param {(Array|Object)=} keys
1030 * @param {Object=} options
1031 * @return {!RegExp}
1032 */
1033function pathToRegexp (path, keys, options) {
1034 if (!isarray(keys)) {
1035 options = /** @type {!Object} */ (keys || options);
1036 keys = [];
1037 }
1038
1039 options = options || {};
1040
1041 if (path instanceof RegExp) {
1042 return regexpToRegexp(path, /** @type {!Array} */ (keys))
1043 }
1044
1045 if (isarray(path)) {
1046 return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options)
1047 }
1048
1049 return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options)
1050}
1051pathToRegexp_1.parse = parse_1;
1052pathToRegexp_1.compile = compile_1;
1053pathToRegexp_1.tokensToFunction = tokensToFunction_1;
1054pathToRegexp_1.tokensToRegExp = tokensToRegExp_1;
1055
1056/* */
1057
1058// $flow-disable-line
1059const regexpCompileCache = Object.create(null);
1060
1061function fillParams (
1062 path,
1063 params,
1064 routeMsg
1065) {
1066 params = params || {};
1067 try {
1068 const filler =
1069 regexpCompileCache[path] ||
1070 (regexpCompileCache[path] = pathToRegexp_1.compile(path));
1071
1072 // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }}
1073 if (params.pathMatch) params[0] = params.pathMatch;
1074
1075 return filler(params, { pretty: true })
1076 } catch (e) {
1077 {
1078 warn(false, `missing param for ${routeMsg}: ${e.message}`);
1079 }
1080 return ''
1081 } finally {
1082 // delete the 0 if it was added
1083 delete params[0];
1084 }
1085}
1086
1087/* */
1088
1089function createRouteMap (
1090 routes,
1091 oldPathList,
1092 oldPathMap,
1093 oldNameMap
1094) {
1095 // the path list is used to control path matching priority
1096 const pathList = oldPathList || [];
1097 // $flow-disable-line
1098 const pathMap = oldPathMap || Object.create(null);
1099 // $flow-disable-line
1100 const nameMap = oldNameMap || Object.create(null);
1101
1102 routes.forEach(route => {
1103 addRouteRecord(pathList, pathMap, nameMap, route);
1104 });
1105
1106 // ensure wildcard routes are always at the end
1107 for (let i = 0, l = pathList.length; i < l; i++) {
1108 if (pathList[i] === '*') {
1109 pathList.push(pathList.splice(i, 1)[0]);
1110 l--;
1111 i--;
1112 }
1113 }
1114
1115 return {
1116 pathList,
1117 pathMap,
1118 nameMap
1119 }
1120}
1121
1122function addRouteRecord (
1123 pathList,
1124 pathMap,
1125 nameMap,
1126 route,
1127 parent,
1128 matchAs
1129) {
1130 const { path, name } = route;
1131 {
1132 assert(path != null, `"path" is required in a route configuration.`);
1133 assert(
1134 typeof route.component !== 'string',
1135 `route config "component" for path: ${String(path || name)} cannot be a ` +
1136 `string id. Use an actual component instead.`
1137 );
1138 }
1139
1140 const pathToRegexpOptions = route.pathToRegexpOptions || {};
1141 const normalizedPath = normalizePath(
1142 path,
1143 parent,
1144 pathToRegexpOptions.strict
1145 );
1146
1147 if (typeof route.caseSensitive === 'boolean') {
1148 pathToRegexpOptions.sensitive = route.caseSensitive;
1149 }
1150
1151 const record = {
1152 path: normalizedPath,
1153 regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
1154 components: route.components || { default: route.component },
1155 instances: {},
1156 name,
1157 parent,
1158 matchAs,
1159 redirect: route.redirect,
1160 beforeEnter: route.beforeEnter,
1161 meta: route.meta || {},
1162 props: route.props == null
1163 ? {}
1164 : route.components
1165 ? route.props
1166 : { default: route.props }
1167 };
1168
1169 if (route.children) {
1170 // Warn if route is named, does not redirect and has a default child route.
1171 // If users navigate to this route by name, the default child will
1172 // not be rendered (GH Issue #629)
1173 {
1174 if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
1175 warn(
1176 false,
1177 `Named Route '${route.name}' has a default child route. ` +
1178 `When navigating to this named route (:to="{name: '${route.name}'"), ` +
1179 `the default child route will not be rendered. Remove the name from ` +
1180 `this route and use the name of the default child route for named ` +
1181 `links instead.`
1182 );
1183 }
1184 }
1185 route.children.forEach(child => {
1186 const childMatchAs = matchAs
1187 ? cleanPath(`${matchAs}/${child.path}`)
1188 : undefined;
1189 addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs);
1190 });
1191 }
1192
1193 if (route.alias !== undefined) {
1194 const aliases = Array.isArray(route.alias)
1195 ? route.alias
1196 : [route.alias];
1197
1198 aliases.forEach(alias => {
1199 const aliasRoute = {
1200 path: alias,
1201 children: route.children
1202 };
1203 addRouteRecord(
1204 pathList,
1205 pathMap,
1206 nameMap,
1207 aliasRoute,
1208 parent,
1209 record.path || '/' // matchAs
1210 );
1211 });
1212 }
1213
1214 if (!pathMap[record.path]) {
1215 pathList.push(record.path);
1216 pathMap[record.path] = record;
1217 }
1218
1219 if (name) {
1220 if (!nameMap[name]) {
1221 nameMap[name] = record;
1222 } else if ("development" !== 'production' && !matchAs) {
1223 warn(
1224 false,
1225 `Duplicate named routes definition: ` +
1226 `{ name: "${name}", path: "${record.path}" }`
1227 );
1228 }
1229 }
1230}
1231
1232function compileRouteRegex (path, pathToRegexpOptions) {
1233 const regex = pathToRegexp_1(path, [], pathToRegexpOptions);
1234 {
1235 const keys = Object.create(null);
1236 regex.keys.forEach(key => {
1237 warn(!keys[key.name], `Duplicate param keys in route with path: "${path}"`);
1238 keys[key.name] = true;
1239 });
1240 }
1241 return regex
1242}
1243
1244function normalizePath (path, parent, strict) {
1245 if (!strict) path = path.replace(/\/$/, '');
1246 if (path[0] === '/') return path
1247 if (parent == null) return path
1248 return cleanPath(`${parent.path}/${path}`)
1249}
1250
1251/* */
1252
1253function normalizeLocation (
1254 raw,
1255 current,
1256 append,
1257 router
1258) {
1259 let next = typeof raw === 'string' ? { path: raw } : raw;
1260 // named target
1261 if (next._normalized) {
1262 return next
1263 } else if (next.name) {
1264 return extend({}, raw)
1265 }
1266
1267 // relative params
1268 if (!next.path && next.params && current) {
1269 next = extend({}, next);
1270 next._normalized = true;
1271 const params = extend(extend({}, current.params), next.params);
1272 if (current.name) {
1273 next.name = current.name;
1274 next.params = params;
1275 } else if (current.matched.length) {
1276 const rawPath = current.matched[current.matched.length - 1].path;
1277 next.path = fillParams(rawPath, params, `path ${current.path}`);
1278 } else {
1279 warn(false, `relative params navigation requires a current route.`);
1280 }
1281 return next
1282 }
1283
1284 const parsedPath = parsePath(next.path || '');
1285 const basePath = (current && current.path) || '/';
1286 const path = parsedPath.path
1287 ? resolvePath(parsedPath.path, basePath, append || next.append)
1288 : basePath;
1289
1290 const query = resolveQuery(
1291 parsedPath.query,
1292 next.query,
1293 router && router.options.parseQuery
1294 );
1295
1296 let hash = next.hash || parsedPath.hash;
1297 if (hash && hash.charAt(0) !== '#') {
1298 hash = `#${hash}`;
1299 }
1300
1301 return {
1302 _normalized: true,
1303 path,
1304 query,
1305 hash
1306 }
1307}
1308
1309/* */
1310
1311
1312
1313function createMatcher (
1314 routes,
1315 router
1316) {
1317 const { pathList, pathMap, nameMap } = createRouteMap(routes);
1318
1319 function addRoutes (routes) {
1320 createRouteMap(routes, pathList, pathMap, nameMap);
1321 }
1322
1323 function match (
1324 raw,
1325 currentRoute,
1326 redirectedFrom
1327 ) {
1328 const location = normalizeLocation(raw, currentRoute, false, router);
1329 const { name } = location;
1330
1331 if (name) {
1332 const record = nameMap[name];
1333 {
1334 warn(record, `Route with name '${name}' does not exist`);
1335 }
1336 if (!record) return _createRoute(null, location)
1337 const paramNames = record.regex.keys
1338 .filter(key => !key.optional)
1339 .map(key => key.name);
1340
1341 if (typeof location.params !== 'object') {
1342 location.params = {};
1343 }
1344
1345 if (currentRoute && typeof currentRoute.params === 'object') {
1346 for (const key in currentRoute.params) {
1347 if (!(key in location.params) && paramNames.indexOf(key) > -1) {
1348 location.params[key] = currentRoute.params[key];
1349 }
1350 }
1351 }
1352
1353 if (record) {
1354 location.path = fillParams(record.path, location.params, `named route "${name}"`);
1355 return _createRoute(record, location, redirectedFrom)
1356 }
1357 } else if (location.path) {
1358 location.params = {};
1359 for (let i = 0; i < pathList.length; i++) {
1360 const path = pathList[i];
1361 const record = pathMap[path];
1362 if (matchRoute(record.regex, location.path, location.params)) {
1363 return _createRoute(record, location, redirectedFrom)
1364 }
1365 }
1366 }
1367 // no match
1368 return _createRoute(null, location)
1369 }
1370
1371 function redirect (
1372 record,
1373 location
1374 ) {
1375 const originalRedirect = record.redirect;
1376 let redirect = typeof originalRedirect === 'function'
1377 ? originalRedirect(createRoute(record, location, null, router))
1378 : originalRedirect;
1379
1380 if (typeof redirect === 'string') {
1381 redirect = { path: redirect };
1382 }
1383
1384 if (!redirect || typeof redirect !== 'object') {
1385 {
1386 warn(
1387 false, `invalid redirect option: ${JSON.stringify(redirect)}`
1388 );
1389 }
1390 return _createRoute(null, location)
1391 }
1392
1393 const re = redirect;
1394 const { name, path } = re;
1395 let { query, hash, params } = location;
1396 query = re.hasOwnProperty('query') ? re.query : query;
1397 hash = re.hasOwnProperty('hash') ? re.hash : hash;
1398 params = re.hasOwnProperty('params') ? re.params : params;
1399
1400 if (name) {
1401 // resolved named direct
1402 const targetRecord = nameMap[name];
1403 {
1404 assert(targetRecord, `redirect failed: named route "${name}" not found.`);
1405 }
1406 return match({
1407 _normalized: true,
1408 name,
1409 query,
1410 hash,
1411 params
1412 }, undefined, location)
1413 } else if (path) {
1414 // 1. resolve relative redirect
1415 const rawPath = resolveRecordPath(path, record);
1416 // 2. resolve params
1417 const resolvedPath = fillParams(rawPath, params, `redirect route with path "${rawPath}"`);
1418 // 3. rematch with existing query and hash
1419 return match({
1420 _normalized: true,
1421 path: resolvedPath,
1422 query,
1423 hash
1424 }, undefined, location)
1425 } else {
1426 {
1427 warn(false, `invalid redirect option: ${JSON.stringify(redirect)}`);
1428 }
1429 return _createRoute(null, location)
1430 }
1431 }
1432
1433 function alias (
1434 record,
1435 location,
1436 matchAs
1437 ) {
1438 const aliasedPath = fillParams(matchAs, location.params, `aliased route with path "${matchAs}"`);
1439 const aliasedMatch = match({
1440 _normalized: true,
1441 path: aliasedPath
1442 });
1443 if (aliasedMatch) {
1444 const matched = aliasedMatch.matched;
1445 const aliasedRecord = matched[matched.length - 1];
1446 location.params = aliasedMatch.params;
1447 return _createRoute(aliasedRecord, location)
1448 }
1449 return _createRoute(null, location)
1450 }
1451
1452 function _createRoute (
1453 record,
1454 location,
1455 redirectedFrom
1456 ) {
1457 if (record && record.redirect) {
1458 return redirect(record, redirectedFrom || location)
1459 }
1460 if (record && record.matchAs) {
1461 return alias(record, location, record.matchAs)
1462 }
1463 return createRoute(record, location, redirectedFrom, router)
1464 }
1465
1466 return {
1467 match,
1468 addRoutes
1469 }
1470}
1471
1472function matchRoute (
1473 regex,
1474 path,
1475 params
1476) {
1477 const m = path.match(regex);
1478
1479 if (!m) {
1480 return false
1481 } else if (!params) {
1482 return true
1483 }
1484
1485 for (let i = 1, len = m.length; i < len; ++i) {
1486 const key = regex.keys[i - 1];
1487 const val = typeof m[i] === 'string' ? decodeURIComponent(m[i]) : m[i];
1488 if (key) {
1489 // Fix #1994: using * with props: true generates a param named 0
1490 params[key.name || 'pathMatch'] = val;
1491 }
1492 }
1493
1494 return true
1495}
1496
1497function resolveRecordPath (path, record) {
1498 return resolvePath(path, record.parent ? record.parent.path : '/', true)
1499}
1500
1501/* */
1502
1503const positionStore = Object.create(null);
1504
1505function setupScroll () {
1506 // Fix for #1585 for Firefox
1507 // Fix for #2195 Add optional third attribute to workaround a bug in safari https://bugs.webkit.org/show_bug.cgi?id=182678
1508 window.history.replaceState({ key: getStateKey() }, '', window.location.href.replace(window.location.origin, ''));
1509 window.addEventListener('popstate', e => {
1510 saveScrollPosition();
1511 if (e.state && e.state.key) {
1512 setStateKey(e.state.key);
1513 }
1514 });
1515}
1516
1517function handleScroll (
1518 router,
1519 to,
1520 from,
1521 isPop
1522) {
1523 if (!router.app) {
1524 return
1525 }
1526
1527 const behavior = router.options.scrollBehavior;
1528 if (!behavior) {
1529 return
1530 }
1531
1532 {
1533 assert(typeof behavior === 'function', `scrollBehavior must be a function`);
1534 }
1535
1536 // wait until re-render finishes before scrolling
1537 router.app.$nextTick(() => {
1538 const position = getScrollPosition();
1539 const shouldScroll = behavior.call(router, to, from, isPop ? position : null);
1540
1541 if (!shouldScroll) {
1542 return
1543 }
1544
1545 if (typeof shouldScroll.then === 'function') {
1546 shouldScroll.then(shouldScroll => {
1547 scrollToPosition((shouldScroll), position);
1548 }).catch(err => {
1549 {
1550 assert(false, err.toString());
1551 }
1552 });
1553 } else {
1554 scrollToPosition(shouldScroll, position);
1555 }
1556 });
1557}
1558
1559function saveScrollPosition () {
1560 const key = getStateKey();
1561 if (key) {
1562 positionStore[key] = {
1563 x: window.pageXOffset,
1564 y: window.pageYOffset
1565 };
1566 }
1567}
1568
1569function getScrollPosition () {
1570 const key = getStateKey();
1571 if (key) {
1572 return positionStore[key]
1573 }
1574}
1575
1576function getElementPosition (el, offset) {
1577 const docEl = document.documentElement;
1578 const docRect = docEl.getBoundingClientRect();
1579 const elRect = el.getBoundingClientRect();
1580 return {
1581 x: elRect.left - docRect.left - offset.x,
1582 y: elRect.top - docRect.top - offset.y
1583 }
1584}
1585
1586function isValidPosition (obj) {
1587 return isNumber(obj.x) || isNumber(obj.y)
1588}
1589
1590function normalizePosition (obj) {
1591 return {
1592 x: isNumber(obj.x) ? obj.x : window.pageXOffset,
1593 y: isNumber(obj.y) ? obj.y : window.pageYOffset
1594 }
1595}
1596
1597function normalizeOffset (obj) {
1598 return {
1599 x: isNumber(obj.x) ? obj.x : 0,
1600 y: isNumber(obj.y) ? obj.y : 0
1601 }
1602}
1603
1604function isNumber (v) {
1605 return typeof v === 'number'
1606}
1607
1608function scrollToPosition (shouldScroll, position) {
1609 const isObject = typeof shouldScroll === 'object';
1610 if (isObject && typeof shouldScroll.selector === 'string') {
1611 const el = document.querySelector(shouldScroll.selector);
1612 if (el) {
1613 let offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {};
1614 offset = normalizeOffset(offset);
1615 position = getElementPosition(el, offset);
1616 } else if (isValidPosition(shouldScroll)) {
1617 position = normalizePosition(shouldScroll);
1618 }
1619 } else if (isObject && isValidPosition(shouldScroll)) {
1620 position = normalizePosition(shouldScroll);
1621 }
1622
1623 if (position) {
1624 window.scrollTo(position.x, position.y);
1625 }
1626}
1627
1628/* */
1629
1630const supportsPushState = inBrowser && (function () {
1631 const ua = window.navigator.userAgent;
1632
1633 if (
1634 (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
1635 ua.indexOf('Mobile Safari') !== -1 &&
1636 ua.indexOf('Chrome') === -1 &&
1637 ua.indexOf('Windows Phone') === -1
1638 ) {
1639 return false
1640 }
1641
1642 return window.history && 'pushState' in window.history
1643})();
1644
1645// use User Timing api (if present) for more accurate key precision
1646const Time = inBrowser && window.performance && window.performance.now
1647 ? window.performance
1648 : Date;
1649
1650let _key = genKey();
1651
1652function genKey () {
1653 return Time.now().toFixed(3)
1654}
1655
1656function getStateKey () {
1657 return _key
1658}
1659
1660function setStateKey (key) {
1661 _key = key;
1662}
1663
1664function pushState (url, replace) {
1665 saveScrollPosition();
1666 // try...catch the pushState call to get around Safari
1667 // DOM Exception 18 where it limits to 100 pushState calls
1668 const history = window.history;
1669 try {
1670 if (replace) {
1671 history.replaceState({ key: _key }, '', url);
1672 } else {
1673 _key = genKey();
1674 history.pushState({ key: _key }, '', url);
1675 }
1676 } catch (e) {
1677 window.location[replace ? 'replace' : 'assign'](url);
1678 }
1679}
1680
1681function replaceState (url) {
1682 pushState(url, true);
1683}
1684
1685/* */
1686
1687function runQueue (queue, fn, cb) {
1688 const step = index => {
1689 if (index >= queue.length) {
1690 cb();
1691 } else {
1692 if (queue[index]) {
1693 fn(queue[index], () => {
1694 step(index + 1);
1695 });
1696 } else {
1697 step(index + 1);
1698 }
1699 }
1700 };
1701 step(0);
1702}
1703
1704/* */
1705
1706function resolveAsyncComponents (matched) {
1707 return (to, from, next) => {
1708 let hasAsync = false;
1709 let pending = 0;
1710 let error = null;
1711
1712 flatMapComponents(matched, (def, _, match, key) => {
1713 // if it's a function and doesn't have cid attached,
1714 // assume it's an async component resolve function.
1715 // we are not using Vue's default async resolving mechanism because
1716 // we want to halt the navigation until the incoming component has been
1717 // resolved.
1718 if (typeof def === 'function' && def.cid === undefined) {
1719 hasAsync = true;
1720 pending++;
1721
1722 const resolve = once(resolvedDef => {
1723 if (isESModule(resolvedDef)) {
1724 resolvedDef = resolvedDef.default;
1725 }
1726 // save resolved on async factory in case it's used elsewhere
1727 def.resolved = typeof resolvedDef === 'function'
1728 ? resolvedDef
1729 : _Vue.extend(resolvedDef);
1730 match.components[key] = resolvedDef;
1731 pending--;
1732 if (pending <= 0) {
1733 next();
1734 }
1735 });
1736
1737 const reject = once(reason => {
1738 const msg = `Failed to resolve async component ${key}: ${reason}`;
1739 "development" !== 'production' && warn(false, msg);
1740 if (!error) {
1741 error = isError(reason)
1742 ? reason
1743 : new Error(msg);
1744 next(error);
1745 }
1746 });
1747
1748 let res;
1749 try {
1750 res = def(resolve, reject);
1751 } catch (e) {
1752 reject(e);
1753 }
1754 if (res) {
1755 if (typeof res.then === 'function') {
1756 res.then(resolve, reject);
1757 } else {
1758 // new syntax in Vue 2.3
1759 const comp = res.component;
1760 if (comp && typeof comp.then === 'function') {
1761 comp.then(resolve, reject);
1762 }
1763 }
1764 }
1765 }
1766 });
1767
1768 if (!hasAsync) next();
1769 }
1770}
1771
1772function flatMapComponents (
1773 matched,
1774 fn
1775) {
1776 return flatten(matched.map(m => {
1777 return Object.keys(m.components).map(key => fn(
1778 m.components[key],
1779 m.instances[key],
1780 m, key
1781 ))
1782 }))
1783}
1784
1785function flatten (arr) {
1786 return Array.prototype.concat.apply([], arr)
1787}
1788
1789const hasSymbol =
1790 typeof Symbol === 'function' &&
1791 typeof Symbol.toStringTag === 'symbol';
1792
1793function isESModule (obj) {
1794 return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module')
1795}
1796
1797// in Webpack 2, require.ensure now also returns a Promise
1798// so the resolve/reject functions may get called an extra time
1799// if the user uses an arrow function shorthand that happens to
1800// return that Promise.
1801function once (fn) {
1802 let called = false;
1803 return function (...args) {
1804 if (called) return
1805 called = true;
1806 return fn.apply(this, args)
1807 }
1808}
1809
1810/* */
1811
1812class History {
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823 // implemented by sub-classes
1824
1825
1826
1827
1828
1829
1830 constructor (router, base) {
1831 this.router = router;
1832 this.base = normalizeBase(base);
1833 // start with a route object that stands for "nowhere"
1834 this.current = START;
1835 this.pending = null;
1836 this.ready = false;
1837 this.readyCbs = [];
1838 this.readyErrorCbs = [];
1839 this.errorCbs = [];
1840 }
1841
1842 listen (cb) {
1843 this.cb = cb;
1844 }
1845
1846 onReady (cb, errorCb) {
1847 if (this.ready) {
1848 cb();
1849 } else {
1850 this.readyCbs.push(cb);
1851 if (errorCb) {
1852 this.readyErrorCbs.push(errorCb);
1853 }
1854 }
1855 }
1856
1857 onError (errorCb) {
1858 this.errorCbs.push(errorCb);
1859 }
1860
1861 transitionTo (location, onComplete, onAbort) {
1862 const route = this.router.match(location, this.current);
1863 this.confirmTransition(route, () => {
1864 this.updateRoute(route);
1865 onComplete && onComplete(route);
1866 this.ensureURL();
1867
1868 // fire ready cbs once
1869 if (!this.ready) {
1870 this.ready = true;
1871 this.readyCbs.forEach(cb => { cb(route); });
1872 }
1873 }, err => {
1874 if (onAbort) {
1875 onAbort(err);
1876 }
1877 if (err && !this.ready) {
1878 this.ready = true;
1879 this.readyErrorCbs.forEach(cb => { cb(err); });
1880 }
1881 });
1882 }
1883
1884 confirmTransition (route, onComplete, onAbort) {
1885 const current = this.current;
1886 const abort = err => {
1887 if (isError(err)) {
1888 if (this.errorCbs.length) {
1889 this.errorCbs.forEach(cb => { cb(err); });
1890 } else {
1891 warn(false, 'uncaught error during route navigation:');
1892 console.error(err);
1893 }
1894 }
1895 onAbort && onAbort(err);
1896 };
1897 if (
1898 isSameRoute(route, current) &&
1899 // in the case the route map has been dynamically appended to
1900 route.matched.length === current.matched.length
1901 ) {
1902 this.ensureURL();
1903 return abort()
1904 }
1905
1906 const {
1907 updated,
1908 deactivated,
1909 activated
1910 } = resolveQueue(this.current.matched, route.matched);
1911
1912 const queue = [].concat(
1913 // in-component leave guards
1914 extractLeaveGuards(deactivated),
1915 // global before hooks
1916 this.router.beforeHooks,
1917 // in-component update hooks
1918 extractUpdateHooks(updated),
1919 // in-config enter guards
1920 activated.map(m => m.beforeEnter),
1921 // async components
1922 resolveAsyncComponents(activated)
1923 );
1924
1925 this.pending = route;
1926 const iterator = (hook, next) => {
1927 if (this.pending !== route) {
1928 return abort()
1929 }
1930 try {
1931 hook(route, current, (to) => {
1932 if (to === false || isError(to)) {
1933 // next(false) -> abort navigation, ensure current URL
1934 this.ensureURL(true);
1935 abort(to);
1936 } else if (
1937 typeof to === 'string' ||
1938 (typeof to === 'object' && (
1939 typeof to.path === 'string' ||
1940 typeof to.name === 'string'
1941 ))
1942 ) {
1943 // next('/') or next({ path: '/' }) -> redirect
1944 abort();
1945 if (typeof to === 'object' && to.replace) {
1946 this.replace(to);
1947 } else {
1948 this.push(to);
1949 }
1950 } else {
1951 // confirm transition and pass on the value
1952 next(to);
1953 }
1954 });
1955 } catch (e) {
1956 abort(e);
1957 }
1958 };
1959
1960 runQueue(queue, iterator, () => {
1961 const postEnterCbs = [];
1962 const isValid = () => this.current === route;
1963 // wait until async components are resolved before
1964 // extracting in-component enter guards
1965 const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
1966 const queue = enterGuards.concat(this.router.resolveHooks);
1967 runQueue(queue, iterator, () => {
1968 if (this.pending !== route) {
1969 return abort()
1970 }
1971 this.pending = null;
1972 onComplete(route);
1973 if (this.router.app) {
1974 this.router.app.$nextTick(() => {
1975 postEnterCbs.forEach(cb => { cb(); });
1976 });
1977 }
1978 });
1979 });
1980 }
1981
1982 updateRoute (route) {
1983 const prev = this.current;
1984 this.current = route;
1985 this.cb && this.cb(route);
1986 this.router.afterHooks.forEach(hook => {
1987 hook && hook(route, prev);
1988 });
1989 }
1990}
1991
1992function normalizeBase (base) {
1993 if (!base) {
1994 if (inBrowser) {
1995 // respect <base> tag
1996 const baseEl = document.querySelector('base');
1997 base = (baseEl && baseEl.getAttribute('href')) || '/';
1998 // strip full URL origin
1999 base = base.replace(/^https?:\/\/[^\/]+/, '');
2000 } else {
2001 base = '/';
2002 }
2003 }
2004 // make sure there's the starting slash
2005 if (base.charAt(0) !== '/') {
2006 base = '/' + base;
2007 }
2008 // remove trailing slash
2009 return base.replace(/\/$/, '')
2010}
2011
2012function resolveQueue (
2013 current,
2014 next
2015) {
2016 let i;
2017 const max = Math.max(current.length, next.length);
2018 for (i = 0; i < max; i++) {
2019 if (current[i] !== next[i]) {
2020 break
2021 }
2022 }
2023 return {
2024 updated: next.slice(0, i),
2025 activated: next.slice(i),
2026 deactivated: current.slice(i)
2027 }
2028}
2029
2030function extractGuards (
2031 records,
2032 name,
2033 bind,
2034 reverse
2035) {
2036 const guards = flatMapComponents(records, (def, instance, match, key) => {
2037 const guard = extractGuard(def, name);
2038 if (guard) {
2039 return Array.isArray(guard)
2040 ? guard.map(guard => bind(guard, instance, match, key))
2041 : bind(guard, instance, match, key)
2042 }
2043 });
2044 return flatten(reverse ? guards.reverse() : guards)
2045}
2046
2047function extractGuard (
2048 def,
2049 key
2050) {
2051 if (typeof def !== 'function') {
2052 // extend now so that global mixins are applied.
2053 def = _Vue.extend(def);
2054 }
2055 return def.options[key]
2056}
2057
2058function extractLeaveGuards (deactivated) {
2059 return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
2060}
2061
2062function extractUpdateHooks (updated) {
2063 return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
2064}
2065
2066function bindGuard (guard, instance) {
2067 if (instance) {
2068 return function boundRouteGuard () {
2069 return guard.apply(instance, arguments)
2070 }
2071 }
2072}
2073
2074function extractEnterGuards (
2075 activated,
2076 cbs,
2077 isValid
2078) {
2079 return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
2080 return bindEnterGuard(guard, match, key, cbs, isValid)
2081 })
2082}
2083
2084function bindEnterGuard (
2085 guard,
2086 match,
2087 key,
2088 cbs,
2089 isValid
2090) {
2091 return function routeEnterGuard (to, from, next) {
2092 return guard(to, from, cb => {
2093 next(cb);
2094 if (typeof cb === 'function') {
2095 cbs.push(() => {
2096 // #750
2097 // if a router-view is wrapped with an out-in transition,
2098 // the instance may not have been registered at this time.
2099 // we will need to poll for registration until current route
2100 // is no longer valid.
2101 poll(cb, match.instances, key, isValid);
2102 });
2103 }
2104 })
2105 }
2106}
2107
2108function poll (
2109 cb, // somehow flow cannot infer this is a function
2110 instances,
2111 key,
2112 isValid
2113) {
2114 if (
2115 instances[key] &&
2116 !instances[key]._isBeingDestroyed // do not reuse being destroyed instance
2117 ) {
2118 cb(instances[key]);
2119 } else if (isValid()) {
2120 setTimeout(() => {
2121 poll(cb, instances, key, isValid);
2122 }, 16);
2123 }
2124}
2125
2126/* */
2127
2128class HTML5History extends History {
2129 constructor (router, base) {
2130 super(router, base);
2131
2132 const expectScroll = router.options.scrollBehavior;
2133 const supportsScroll = supportsPushState && expectScroll;
2134
2135 if (supportsScroll) {
2136 setupScroll();
2137 }
2138
2139 const initLocation = getLocation(this.base);
2140 window.addEventListener('popstate', e => {
2141 const current = this.current;
2142
2143 // Avoiding first `popstate` event dispatched in some browsers but first
2144 // history route not updated since async guard at the same time.
2145 const location = getLocation(this.base);
2146 if (this.current === START && location === initLocation) {
2147 return
2148 }
2149
2150 this.transitionTo(location, route => {
2151 if (supportsScroll) {
2152 handleScroll(router, route, current, true);
2153 }
2154 });
2155 });
2156 }
2157
2158 go (n) {
2159 window.history.go(n);
2160 }
2161
2162 push (location, onComplete, onAbort) {
2163 const { current: fromRoute } = this;
2164 this.transitionTo(location, route => {
2165 pushState(cleanPath(this.base + route.fullPath));
2166 handleScroll(this.router, route, fromRoute, false);
2167 onComplete && onComplete(route);
2168 }, onAbort);
2169 }
2170
2171 replace (location, onComplete, onAbort) {
2172 const { current: fromRoute } = this;
2173 this.transitionTo(location, route => {
2174 replaceState(cleanPath(this.base + route.fullPath));
2175 handleScroll(this.router, route, fromRoute, false);
2176 onComplete && onComplete(route);
2177 }, onAbort);
2178 }
2179
2180 ensureURL (push) {
2181 if (getLocation(this.base) !== this.current.fullPath) {
2182 const current = cleanPath(this.base + this.current.fullPath);
2183 push ? pushState(current) : replaceState(current);
2184 }
2185 }
2186
2187 getCurrentLocation () {
2188 return getLocation(this.base)
2189 }
2190}
2191
2192function getLocation (base) {
2193 let path = decodeURI(window.location.pathname);
2194 if (base && path.indexOf(base) === 0) {
2195 path = path.slice(base.length);
2196 }
2197 return (path || '/') + window.location.search + window.location.hash
2198}
2199
2200/* */
2201
2202class HashHistory extends History {
2203 constructor (router, base, fallback) {
2204 super(router, base);
2205 // check history fallback deeplinking
2206 if (fallback && checkFallback(this.base)) {
2207 return
2208 }
2209 ensureSlash();
2210 }
2211
2212 // this is delayed until the app mounts
2213 // to avoid the hashchange listener being fired too early
2214 setupListeners () {
2215 const router = this.router;
2216 const expectScroll = router.options.scrollBehavior;
2217 const supportsScroll = supportsPushState && expectScroll;
2218
2219 if (supportsScroll) {
2220 setupScroll();
2221 }
2222
2223 window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
2224 const current = this.current;
2225 if (!ensureSlash()) {
2226 return
2227 }
2228 this.transitionTo(getHash(), route => {
2229 if (supportsScroll) {
2230 handleScroll(this.router, route, current, true);
2231 }
2232 if (!supportsPushState) {
2233 replaceHash(route.fullPath);
2234 }
2235 });
2236 });
2237 }
2238
2239 push (location, onComplete, onAbort) {
2240 const { current: fromRoute } = this;
2241 this.transitionTo(location, route => {
2242 pushHash(route.fullPath);
2243 handleScroll(this.router, route, fromRoute, false);
2244 onComplete && onComplete(route);
2245 }, onAbort);
2246 }
2247
2248 replace (location, onComplete, onAbort) {
2249 const { current: fromRoute } = this;
2250 this.transitionTo(location, route => {
2251 replaceHash(route.fullPath);
2252 handleScroll(this.router, route, fromRoute, false);
2253 onComplete && onComplete(route);
2254 }, onAbort);
2255 }
2256
2257 go (n) {
2258 window.history.go(n);
2259 }
2260
2261 ensureURL (push) {
2262 const current = this.current.fullPath;
2263 if (getHash() !== current) {
2264 push ? pushHash(current) : replaceHash(current);
2265 }
2266 }
2267
2268 getCurrentLocation () {
2269 return getHash()
2270 }
2271}
2272
2273function checkFallback (base) {
2274 const location = getLocation(base);
2275 if (!/^\/#/.test(location)) {
2276 window.location.replace(
2277 cleanPath(base + '/#' + location)
2278 );
2279 return true
2280 }
2281}
2282
2283function ensureSlash () {
2284 const path = getHash();
2285 if (path.charAt(0) === '/') {
2286 return true
2287 }
2288 replaceHash('/' + path);
2289 return false
2290}
2291
2292function getHash () {
2293 // We can't use window.location.hash here because it's not
2294 // consistent across browsers - Firefox will pre-decode it!
2295 let href = window.location.href;
2296 const index = href.indexOf('#');
2297 // empty path
2298 if (index < 0) return ''
2299
2300 href = href.slice(index + 1);
2301 // decode the hash but not the search or hash
2302 // as search(query) is already decoded
2303 // https://github.com/vuejs/vue-router/issues/2708
2304 const searchIndex = href.indexOf('?');
2305 if (searchIndex < 0) {
2306 const hashIndex = href.indexOf('#');
2307 if (hashIndex > -1) href = decodeURI(href.slice(0, hashIndex)) + href.slice(hashIndex);
2308 else href = decodeURI(href);
2309 } else {
2310 if (searchIndex > -1) href = decodeURI(href.slice(0, searchIndex)) + href.slice(searchIndex);
2311 }
2312
2313 return href
2314}
2315
2316function getUrl (path) {
2317 const href = window.location.href;
2318 const i = href.indexOf('#');
2319 const base = i >= 0 ? href.slice(0, i) : href;
2320 return `${base}#${path}`
2321}
2322
2323function pushHash (path) {
2324 if (supportsPushState) {
2325 pushState(getUrl(path));
2326 } else {
2327 window.location.hash = path;
2328 }
2329}
2330
2331function replaceHash (path) {
2332 if (supportsPushState) {
2333 replaceState(getUrl(path));
2334 } else {
2335 window.location.replace(getUrl(path));
2336 }
2337}
2338
2339/* */
2340
2341class AbstractHistory extends History {
2342
2343
2344
2345 constructor (router, base) {
2346 super(router, base);
2347 this.stack = [];
2348 this.index = -1;
2349 }
2350
2351 push (location, onComplete, onAbort) {
2352 this.transitionTo(location, route => {
2353 this.stack = this.stack.slice(0, this.index + 1).concat(route);
2354 this.index++;
2355 onComplete && onComplete(route);
2356 }, onAbort);
2357 }
2358
2359 replace (location, onComplete, onAbort) {
2360 this.transitionTo(location, route => {
2361 this.stack = this.stack.slice(0, this.index).concat(route);
2362 onComplete && onComplete(route);
2363 }, onAbort);
2364 }
2365
2366 go (n) {
2367 const targetIndex = this.index + n;
2368 if (targetIndex < 0 || targetIndex >= this.stack.length) {
2369 return
2370 }
2371 const route = this.stack[targetIndex];
2372 this.confirmTransition(route, () => {
2373 this.index = targetIndex;
2374 this.updateRoute(route);
2375 });
2376 }
2377
2378 getCurrentLocation () {
2379 const current = this.stack[this.stack.length - 1];
2380 return current ? current.fullPath : '/'
2381 }
2382
2383 ensureURL () {
2384 // noop
2385 }
2386}
2387
2388/* */
2389
2390
2391
2392class VueRouter {
2393
2394
2395
2396
2397
2398
2399
2400
2401
2402
2403
2404
2405
2406
2407
2408
2409 constructor (options = {}) {
2410 this.app = null;
2411 this.apps = [];
2412 this.options = options;
2413 this.beforeHooks = [];
2414 this.resolveHooks = [];
2415 this.afterHooks = [];
2416 this.matcher = createMatcher(options.routes || [], this);
2417
2418 let mode = options.mode || 'hash';
2419 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false;
2420 if (this.fallback) {
2421 mode = 'hash';
2422 }
2423 if (!inBrowser) {
2424 mode = 'abstract';
2425 }
2426 this.mode = mode;
2427
2428 switch (mode) {
2429 case 'history':
2430 this.history = new HTML5History(this, options.base);
2431 break
2432 case 'hash':
2433 this.history = new HashHistory(this, options.base, this.fallback);
2434 break
2435 case 'abstract':
2436 this.history = new AbstractHistory(this, options.base);
2437 break
2438 default:
2439 {
2440 assert(false, `invalid mode: ${mode}`);
2441 }
2442 }
2443 }
2444
2445 match (
2446 raw,
2447 current,
2448 redirectedFrom
2449 ) {
2450 return this.matcher.match(raw, current, redirectedFrom)
2451 }
2452
2453 get currentRoute () {
2454 return this.history && this.history.current
2455 }
2456
2457 init (app /* Vue component instance */) {
2458 "development" !== 'production' && assert(
2459 install.installed,
2460 `not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
2461 `before creating root instance.`
2462 );
2463
2464 this.apps.push(app);
2465
2466 // set up app destroyed handler
2467 // https://github.com/vuejs/vue-router/issues/2639
2468 app.$once('hook:destroyed', () => {
2469 // clean out app from this.apps array once destroyed
2470 const index = this.apps.indexOf(app);
2471 if (index > -1) this.apps.splice(index, 1);
2472 // ensure we still have a main app or null if no apps
2473 // we do not release the router so it can be reused
2474 if (this.app === app) this.app = this.apps[0] || null;
2475 });
2476
2477 // main app previously initialized
2478 // return as we don't need to set up new history listener
2479 if (this.app) {
2480 return
2481 }
2482
2483 this.app = app;
2484
2485 const history = this.history;
2486
2487 if (history instanceof HTML5History) {
2488 history.transitionTo(history.getCurrentLocation());
2489 } else if (history instanceof HashHistory) {
2490 const setupHashListener = () => {
2491 history.setupListeners();
2492 };
2493 history.transitionTo(
2494 history.getCurrentLocation(),
2495 setupHashListener,
2496 setupHashListener
2497 );
2498 }
2499
2500 history.listen(route => {
2501 this.apps.forEach((app) => {
2502 app._route = route;
2503 });
2504 });
2505 }
2506
2507 beforeEach (fn) {
2508 return registerHook(this.beforeHooks, fn)
2509 }
2510
2511 beforeResolve (fn) {
2512 return registerHook(this.resolveHooks, fn)
2513 }
2514
2515 afterEach (fn) {
2516 return registerHook(this.afterHooks, fn)
2517 }
2518
2519 onReady (cb, errorCb) {
2520 this.history.onReady(cb, errorCb);
2521 }
2522
2523 onError (errorCb) {
2524 this.history.onError(errorCb);
2525 }
2526
2527 push (location, onComplete, onAbort) {
2528 this.history.push(location, onComplete, onAbort);
2529 }
2530
2531 replace (location, onComplete, onAbort) {
2532 this.history.replace(location, onComplete, onAbort);
2533 }
2534
2535 go (n) {
2536 this.history.go(n);
2537 }
2538
2539 back () {
2540 this.go(-1);
2541 }
2542
2543 forward () {
2544 this.go(1);
2545 }
2546
2547 getMatchedComponents (to) {
2548 const route = to
2549 ? to.matched
2550 ? to
2551 : this.resolve(to).route
2552 : this.currentRoute;
2553 if (!route) {
2554 return []
2555 }
2556 return [].concat.apply([], route.matched.map(m => {
2557 return Object.keys(m.components).map(key => {
2558 return m.components[key]
2559 })
2560 }))
2561 }
2562
2563 resolve (
2564 to,
2565 current,
2566 append
2567 ) {
2568 current = current || this.history.current;
2569 const location = normalizeLocation(
2570 to,
2571 current,
2572 append,
2573 this
2574 );
2575 const route = this.match(location, current);
2576 const fullPath = route.redirectedFrom || route.fullPath;
2577 const base = this.history.base;
2578 const href = createHref(base, fullPath, this.mode);
2579 return {
2580 location,
2581 route,
2582 href,
2583 // for backwards compat
2584 normalizedTo: location,
2585 resolved: route
2586 }
2587 }
2588
2589 addRoutes (routes) {
2590 this.matcher.addRoutes(routes);
2591 if (this.history.current !== START) {
2592 this.history.transitionTo(this.history.getCurrentLocation());
2593 }
2594 }
2595}
2596
2597function registerHook (list, fn) {
2598 list.push(fn);
2599 return () => {
2600 const i = list.indexOf(fn);
2601 if (i > -1) list.splice(i, 1);
2602 }
2603}
2604
2605function createHref (base, fullPath, mode) {
2606 var path = mode === 'hash' ? '#' + fullPath : fullPath;
2607 return base ? cleanPath(base + '/' + path) : path
2608}
2609
2610VueRouter.install = install;
2611VueRouter.version = '3.0.4';
2612
2613if (inBrowser && window.Vue) {
2614 window.Vue.use(VueRouter);
2615}
2616
2617export default VueRouter;