UNPKG

79.5 kBJavaScriptView Raw
1/*!
2 * vue-router v4.0.0-alpha.6
3 * (c) 2020 Eduardo San Martin Morote
4 * @license MIT
5 */
6'use strict';
7
8Object.defineProperty(exports, '__esModule', { value: true });
9
10var vue = require('vue');
11
12const hasSymbol = typeof Symbol === 'function' && typeof Symbol.toStringTag === 'symbol';
13const PolySymbol = (name) =>
14// vr = vue router
15hasSymbol ? Symbol(name) : `_vr_` + name;
16// rvlm = Router View Location Matched
17const matchedRouteKey = PolySymbol('rvlm');
18// rvd = Router View Depth
19const viewDepthKey = PolySymbol('rvd');
20// r = router
21const routerKey = PolySymbol('r');
22// rt = route location
23const routeLocationKey = PolySymbol('rl');
24
25const isBrowser = typeof window !== 'undefined';
26
27function isESModule(obj) {
28 return obj.__esModule || (hasSymbol && obj[Symbol.toStringTag] === 'Module');
29}
30function applyToParams(fn, params) {
31 const newParams = {};
32 for (const key in params) {
33 const value = params[key];
34 newParams[key] = Array.isArray(value) ? value.map(fn) : fn(value);
35 }
36 return newParams;
37}
38function isSameRouteRecord(a, b) {
39 // since the original record has an undefined value for aliasOf
40 // but all aliases point to the original record, this will always compare
41 // the original record
42 return (a.aliasOf || a) === (b.aliasOf || b);
43}
44function isSameLocationObject(a, b) {
45 if (Object.keys(a).length !== Object.keys(b).length)
46 return false;
47 for (let key in a) {
48 if (!isSameLocationObjectValue(a[key], b[key]))
49 return false;
50 }
51 return true;
52}
53function isSameLocationObjectValue(a, b) {
54 return Array.isArray(a)
55 ? isEquivalentArray(a, b)
56 : Array.isArray(b)
57 ? isEquivalentArray(b, a)
58 : a === b;
59}
60/**
61 * Check if two arrays are the same or if an array with one single entry is the
62 * same as another primitive value. Used to check query and parameters
63 *
64 * @param a array of values
65 * @param b array of values or a single value
66 */
67function isEquivalentArray(a, b) {
68 return Array.isArray(b)
69 ? a.length === b.length && a.every((value, i) => value === b[i])
70 : a.length === 1 && a[0] === b;
71}
72
73var NavigationType;
74(function (NavigationType) {
75 NavigationType["pop"] = "pop";
76 NavigationType["push"] = "push";
77})(NavigationType || (NavigationType = {}));
78var NavigationDirection;
79(function (NavigationDirection) {
80 NavigationDirection["back"] = "back";
81 NavigationDirection["forward"] = "forward";
82 NavigationDirection["unknown"] = "";
83})(NavigationDirection || (NavigationDirection = {}));
84/**
85 * Starting location for Histories
86 */
87const START_PATH = '';
88const START = {
89 fullPath: START_PATH,
90};
91// Generic utils
92function normalizeHistoryLocation(location) {
93 return {
94 // to avoid doing a typeof or in that is quite long
95 fullPath: location.fullPath || location,
96 };
97}
98/**
99 * Normalizes a base by removing any trailing slash and reading the base tag if
100 * present.
101 *
102 * @param base base to normalize
103 */
104function normalizeBase(base) {
105 if (!base) {
106 if (isBrowser) {
107 // respect <base> tag
108 const baseEl = document.querySelector('base');
109 base = (baseEl && baseEl.getAttribute('href')) || '/';
110 // strip full URL origin
111 base = base.replace(/^\w+:\/\/[^\/]+/, '');
112 }
113 else {
114 base = '/';
115 }
116 }
117 // ensure leading slash when it was removed by the regex above avoid leading
118 // slash with hash because the file could be read from the disk like file://
119 // and the leading slash would cause problems
120 if (base.charAt(0) !== '/' && base.charAt(0) !== '#')
121 base = '/' + base;
122 // remove the trailing slash so all other method can just do `base + fullPath`
123 // to build an href
124 return base.replace(/\/$/, '');
125}
126
127// import { RouteLocationNormalized } from '../types'
128function computeScrollPosition(el) {
129 return el
130 ? {
131 x: el.scrollLeft,
132 y: el.scrollTop,
133 }
134 : {
135 x: window.pageXOffset,
136 y: window.pageYOffset,
137 };
138}
139function getElementPosition(el, offset) {
140 const docEl = document.documentElement;
141 const docRect = docEl.getBoundingClientRect();
142 const elRect = el.getBoundingClientRect();
143 return {
144 x: elRect.left - docRect.left - offset.x,
145 y: elRect.top - docRect.top - offset.y,
146 };
147}
148const hashStartsWithNumberRE = /^#\d/;
149function scrollToPosition(position) {
150 let normalizedPosition = null;
151 if ('selector' in position) {
152 // getElementById would still fail if the selector contains a more complicated query like #main[data-attr]
153 // but at the same time, it doesn't make much sense to select an element with an id and an extra selector
154 const el = hashStartsWithNumberRE.test(position.selector)
155 ? document.getElementById(position.selector.slice(1))
156 : document.querySelector(position.selector);
157 if (el) {
158 const offset = position.offset || { x: 0, y: 0 };
159 normalizedPosition = getElementPosition(el, offset);
160 }
161 // TODO: else dev warning?
162 }
163 else {
164 normalizedPosition = {
165 x: position.x,
166 y: position.y,
167 };
168 }
169 if (isBrowser && normalizedPosition) {
170 window.scrollTo(normalizedPosition.x, normalizedPosition.y);
171 }
172}
173/**
174 * TODO: refactor the scroll behavior so it can be tree shaken
175 */
176const scrollPositions = new Map();
177function getScrollKey(path, distance) {
178 const position = isBrowser && history.state ? history.state.position - distance : -1;
179 return position + path;
180}
181function saveScrollOnLeave(key) {
182 scrollPositions.set(key, isBrowser ? computeScrollPosition() : { x: 0, y: 0 });
183}
184function getSavedScroll(key) {
185 return scrollPositions.get(key);
186}
187
188/**
189 * Transforms an URI into a normalized history location
190 *
191 * @param parseQuery
192 * @param location - URI to normalize
193 * @returns a normalized history location
194 */
195function parseURL(parseQuery, location) {
196 let path = '', query = {}, searchString = '', hash = '';
197 // Could use URL and URLSearchParams but IE 11 doesn't support it
198 const searchPos = location.indexOf('?');
199 const hashPos = location.indexOf('#', searchPos > -1 ? searchPos : 0);
200 if (searchPos > -1) {
201 path = location.slice(0, searchPos);
202 searchString = location.slice(searchPos + 1, hashPos > -1 ? hashPos : location.length);
203 query = parseQuery(searchString);
204 }
205 if (hashPos > -1) {
206 path = path || location.slice(0, hashPos);
207 // keep the # character
208 hash = location.slice(hashPos, location.length);
209 }
210 // no search and no query
211 path = path || location;
212 return {
213 fullPath: location,
214 path,
215 query,
216 hash,
217 };
218}
219/**
220 * Stringifies a URL object
221 *
222 * @param stringifyQuery
223 * @param location
224 */
225function stringifyURL(stringifyQuery, location) {
226 let query = location.query ? stringifyQuery(location.query) : '';
227 return location.path + (query && '?') + query + (location.hash || '');
228}
229/**
230 * Strips off the base from the beginning of a location.pathname
231 *
232 * @param pathname - location.pathname
233 * @param base - base to strip off
234 */
235function stripBase(pathname, base) {
236 if (!base || pathname.indexOf(base) !== 0)
237 return pathname;
238 return pathname.replace(base, '') || '/';
239}
240
241/**
242 * Creates a normalized history location from a window.location object
243 * @param location
244 */
245function createCurrentLocation(base, location) {
246 const { pathname, search, hash } = location;
247 // allows hash based url
248 const hashPos = base.indexOf('#');
249 if (hashPos > -1) {
250 // prepend the starting slash to hash so the url starts with /#
251 let pathFromHash = hash.slice(1);
252 if (pathFromHash.charAt(0) !== '/')
253 pathFromHash = '/' + pathFromHash;
254 return normalizeHistoryLocation(stripBase(pathFromHash, ''));
255 }
256 const path = stripBase(pathname, base);
257 return normalizeHistoryLocation(path + search + hash);
258}
259function useHistoryListeners(base, historyState, location, replace) {
260 let listeners = [];
261 let teardowns = [];
262 // TODO: should it be a stack? a Dict. Check if the popstate listener
263 // can trigger twice
264 let pauseState = null;
265 const popStateHandler = ({ state, }) => {
266 const to = createCurrentLocation(base, window.location);
267 if (!state)
268 return replace(to.fullPath);
269 const from = location.value;
270 const fromState = historyState.value;
271 location.value = to;
272 historyState.value = state;
273 // ignore the popstate and reset the pauseState
274 if (pauseState && pauseState.fullPath === from.fullPath) {
275 pauseState = null;
276 return;
277 }
278 const deltaFromCurrent = fromState
279 ? state.position - fromState.position
280 : '';
281 const distance = deltaFromCurrent || 0;
282 // console.log({ deltaFromCurrent })
283 // Here we could also revert the navigation by calling history.go(-distance)
284 // this listener will have to be adapted to not trigger again and to wait for the url
285 // to be updated before triggering the listeners. Some kind of validation function would also
286 // need to be passed to the listeners so the navigation can be accepted
287 // call all listeners
288 listeners.forEach(listener => {
289 listener(location.value, from, {
290 distance,
291 type: NavigationType.pop,
292 direction: distance
293 ? distance > 0
294 ? NavigationDirection.forward
295 : NavigationDirection.back
296 : NavigationDirection.unknown,
297 });
298 });
299 };
300 function pauseListeners() {
301 pauseState = location.value;
302 }
303 function listen(callback) {
304 // setup the listener and prepare teardown callbacks
305 listeners.push(callback);
306 const teardown = () => {
307 const index = listeners.indexOf(callback);
308 if (index > -1)
309 listeners.splice(index, 1);
310 };
311 teardowns.push(teardown);
312 return teardown;
313 }
314 function beforeUnloadListener() {
315 const { history } = window;
316 if (!history.state)
317 return;
318 history.replaceState({
319 ...history.state,
320 scroll: computeScrollPosition(),
321 }, '');
322 }
323 function destroy() {
324 for (const teardown of teardowns)
325 teardown();
326 teardowns = [];
327 window.removeEventListener('popstate', popStateHandler);
328 window.removeEventListener('beforeunload', beforeUnloadListener);
329 }
330 // setup the listeners and prepare teardown callbacks
331 window.addEventListener('popstate', popStateHandler);
332 window.addEventListener('beforeunload', beforeUnloadListener);
333 return {
334 pauseListeners,
335 listen,
336 destroy,
337 };
338}
339/**
340 * Creates a state object
341 */
342function buildState(back, current, forward, replaced = false, computeScroll = false) {
343 return {
344 back,
345 current,
346 forward,
347 replaced,
348 position: window.history.length,
349 scroll: computeScroll ? computeScrollPosition() : null,
350 };
351}
352function useHistoryStateNavigation(base) {
353 const { history } = window;
354 // private variables
355 let location = {
356 value: createCurrentLocation(base, window.location),
357 };
358 let historyState = { value: history.state };
359 // build current history entry as this is a fresh navigation
360 if (!historyState.value) {
361 changeLocation(location.value, {
362 back: null,
363 current: location.value,
364 forward: null,
365 // the length is off by one, we need to decrease it
366 position: history.length - 1,
367 replaced: true,
368 scroll: computeScrollPosition(),
369 }, true);
370 }
371 function changeLocation(to, state, replace) {
372 const url = base + to.fullPath;
373 try {
374 // BROWSER QUIRK
375 // NOTE: Safari throws a SecurityError when calling this function 100 times in 30 seconds
376 history[replace ? 'replaceState' : 'pushState'](state, '', url);
377 historyState.value = state;
378 }
379 catch (err) {
380 vue.warn('[vue-router]: Error with push/replace State', err);
381 // Force the navigation, this also resets the call count
382 window.location[replace ? 'replace' : 'assign'](url);
383 }
384 }
385 function replace(to, data) {
386 const normalized = normalizeHistoryLocation(to);
387 const state = {
388 ...buildState(historyState.value.back,
389 // keep back and forward entries but override current position
390 normalized, historyState.value.forward, true),
391 ...history.state,
392 ...data,
393 position: historyState.value.position,
394 };
395 changeLocation(normalized, state, true);
396 location.value = normalized;
397 }
398 function push(to, data) {
399 const normalized = normalizeHistoryLocation(to);
400 // Add to current entry the information of where we are going
401 // as well as saving the current position
402 // TODO: the scroll position computation should be customizable
403 const currentState = {
404 ...history.state,
405 forward: normalized,
406 scroll: computeScrollPosition(),
407 };
408 changeLocation(currentState.current, currentState, true);
409 const state = {
410 ...buildState(location.value, normalized, null),
411 position: currentState.position + 1,
412 ...data,
413 };
414 changeLocation(normalized, state, false);
415 location.value = normalized;
416 }
417 return {
418 location,
419 state: historyState,
420 push,
421 replace,
422 };
423}
424function createWebHistory(base) {
425 base = normalizeBase(base);
426 const historyNavigation = useHistoryStateNavigation(base);
427 const historyListeners = useHistoryListeners(base, historyNavigation.state, historyNavigation.location, historyNavigation.replace);
428 function go(distance, triggerListeners = true) {
429 if (!triggerListeners)
430 historyListeners.pauseListeners();
431 history.go(distance);
432 }
433 const routerHistory = {
434 // it's overridden right after
435 // @ts-ignore
436 location: '',
437 base,
438 go,
439 ...historyNavigation,
440 ...historyListeners,
441 };
442 Object.defineProperty(routerHistory, 'location', {
443 get: () => historyNavigation.location.value,
444 });
445 Object.defineProperty(routerHistory, 'state', {
446 get: () => historyNavigation.state.value,
447 });
448 return routerHistory;
449}
450
451/**
452 * Creates a in-memory based history. The main purpose of this history is to handle SSR. It starts in a special location that is nowhere.
453 * It's up to the user to replace that location with the starter location.
454 * @param base - Base applied to all urls, defaults to '/'
455 * @returns a history object that can be passed to the router constructor
456 */
457function createMemoryHistory(base = '') {
458 let listeners = [];
459 // TODO: make sure this is right as the first location is nowhere so maybe this should be empty instead
460 let queue = [START];
461 let position = 0;
462 function setLocation(location) {
463 position++;
464 if (position === queue.length) {
465 // we are at the end, we can simply append a new entry
466 queue.push(location);
467 }
468 else {
469 // we are in the middle, we remove everything from here in the queue
470 queue.splice(position);
471 queue.push(location);
472 }
473 }
474 function triggerListeners(to, from, { direction, distance, }) {
475 const info = {
476 direction,
477 distance,
478 type: NavigationType.pop,
479 };
480 for (let callback of listeners) {
481 callback(to, from, info);
482 }
483 }
484 const routerHistory = {
485 // rewritten by Object.defineProperty
486 location: START,
487 // TODO:
488 state: {},
489 base,
490 replace(to) {
491 const toNormalized = normalizeHistoryLocation(to);
492 // remove current entry and decrement position
493 queue.splice(position--, 1);
494 setLocation(toNormalized);
495 },
496 push(to, data) {
497 setLocation(normalizeHistoryLocation(to));
498 },
499 listen(callback) {
500 listeners.push(callback);
501 return () => {
502 const index = listeners.indexOf(callback);
503 if (index > -1)
504 listeners.splice(index, 1);
505 };
506 },
507 destroy() {
508 listeners = [];
509 },
510 go(distance, shouldTrigger = true) {
511 const from = this.location;
512 const direction =
513 // we are considering distance === 0 going forward, but in abstract mode
514 // using 0 for the distance doesn't make sense like it does in html5 where
515 // it reloads the page
516 distance < 0 ? NavigationDirection.back : NavigationDirection.forward;
517 position = Math.max(0, Math.min(position + distance, queue.length - 1));
518 if (shouldTrigger) {
519 triggerListeners(this.location, from, {
520 direction,
521 distance,
522 });
523 }
524 },
525 };
526 Object.defineProperty(routerHistory, 'location', {
527 get: () => queue[position],
528 });
529 return routerHistory;
530}
531
532function createWebHashHistory(base) {
533 // Make sure this implementation is fine in terms of encoding, specially for IE11
534 return createWebHistory(location.host ? normalizeBase(base) + '/#' : '#');
535}
536
537/**
538 * Encoding Rules ␣ = Space Path: ␣ " < > # ? { } Query: ␣ " < > # & = Hash: ␣ "
539 * < > `
540 *
541 * On top of that, the RFC3986 (https://tools.ietf.org/html/rfc3986#section-2.2)
542 * defines some extra characters to be encoded. Most browsers do not encode them
543 * in encodeURI https://github.com/whatwg/url/issues/369, so it may be safer to
544 * also encode `!'()*`. Leaving unencoded only ASCII alphanumeric(`a-zA-Z0-9`)
545 * plus `-._~`. This extra safety should be applied to query by patching the
546 * string returned by encodeURIComponent encodeURI also encodes `[\]^`. `\`
547 * should be encoded to avoid ambiguity. Browsers (IE, FF, C) transform a `\`
548 * into a `/` if directly typed in. The _backtick_ (`````) should also be
549 * encoded everywhere because some browsers like FF encode it when directly
550 * written while others don't. Safari and IE don't encode ``"<>{}``` in hash.
551 */
552// const EXTRA_RESERVED_RE = /[!'()*]/g
553// const encodeReservedReplacer = (c: string) => '%' + c.charCodeAt(0).toString(16)
554const HASH_RE = /#/g; // %23
555const AMPERSAND_RE = /&/g; // %26
556const SLASH_RE = /\//g; // %2F
557const EQUAL_RE = /=/g; // %3D
558const IM_RE = /\?/g; // %3F
559const ENC_BRACKET_OPEN_RE = /%5B/g; // [
560const ENC_BRACKET_CLOSE_RE = /%5D/g; // ]
561const ENC_CARET_RE = /%5E/g; // ^
562const ENC_BACKTICK_RE = /%60/g; // `
563const ENC_CURLY_OPEN_RE = /%7B/g; // {
564const ENC_PIPE_RE = /%7C/g; // |
565const ENC_CURLY_CLOSE_RE = /%7D/g; // }
566/**
567 * Encode characters that need to be encoded on the path, search and hash
568 * sections of the URL.
569 *
570 * @internal
571 * @param text - string to encode
572 * @returns encoded string
573 */
574function commonEncode(text) {
575 return encodeURI('' + text)
576 .replace(ENC_PIPE_RE, '|')
577 .replace(ENC_BRACKET_OPEN_RE, '[')
578 .replace(ENC_BRACKET_CLOSE_RE, ']');
579}
580/**
581 * Encode characters that need to be encoded query keys and values on the query
582 * section of the URL.
583 *
584 * @param text - string to encode
585 * @returns encoded string
586 */
587function encodeQueryProperty(text) {
588 return commonEncode(text)
589 .replace(HASH_RE, '%23')
590 .replace(AMPERSAND_RE, '%26')
591 .replace(EQUAL_RE, '%3D')
592 .replace(ENC_BACKTICK_RE, '`')
593 .replace(ENC_CURLY_OPEN_RE, '{')
594 .replace(ENC_CURLY_CLOSE_RE, '}')
595 .replace(ENC_CARET_RE, '^');
596}
597/**
598 * Encode characters that need to be encoded on the path section of the URL.
599 *
600 * @param text - string to encode
601 * @returns encoded string
602 */
603function encodePath(text) {
604 return commonEncode(text)
605 .replace(HASH_RE, '%23')
606 .replace(IM_RE, '%3F');
607}
608/**
609 * Encode characters that need to be encoded on the path section of the URL as a
610 * param. This function encodes everything {@link encodePath} does plus the
611 * slash (`/`) character.
612 *
613 * @param text - string to encode
614 * @returns encoded string
615 */
616function encodeParam(text) {
617 return encodePath(text).replace(SLASH_RE, '%2F');
618}
619/**
620 * Decode text using `decodeURIComponent`. Returns the original text if it
621 * fails.
622 *
623 * @param text - string to decode
624 * @returns decoded string
625 */
626function decode(text) {
627 try {
628 return decodeURIComponent(text);
629 }
630 catch (err) {
631 vue.warn(`Error decoding "${text}". Using original value`);
632 }
633 return text;
634}
635
636/**
637 * Transforms a queryString into a {@link LocationQuery} object. Accept both, a
638 * version with the leading `?` and without Should work as URLSearchParams
639 *
640 * @param search - search string to parse
641 * @returns a query object
642 */
643function parseQuery(search) {
644 const query = {};
645 // avoid creating an object with an empty key and empty value
646 // because of split('&')
647 if (search === '' || search === '?')
648 return query;
649 const hasLeadingIM = search[0] === '?';
650 const searchParams = (hasLeadingIM ? search.slice(1) : search).split('&');
651 for (let i = 0; i < searchParams.length; ++i) {
652 let [key, rawValue] = searchParams[i].split('=');
653 key = decode(key);
654 // avoid decoding null
655 let value = rawValue == null ? null : decode(rawValue);
656 if (key in query) {
657 // an extra variable for ts types
658 let currentValue = query[key];
659 if (!Array.isArray(currentValue)) {
660 currentValue = query[key] = [currentValue];
661 }
662 currentValue.push(value);
663 }
664 else {
665 query[key] = value;
666 }
667 }
668 return query;
669}
670/**
671 * Stringifies a {@link LocationQueryRaw} object. Like `URLSearchParams`, it
672 * doesn't prepend a `?`
673 *
674 * @param query - query object to stringify
675 * @returns string version of the query without the leading `?`
676 */
677function stringifyQuery(query) {
678 let search = '';
679 for (let key in query) {
680 if (search.length)
681 search += '&';
682 const value = query[key];
683 key = encodeQueryProperty(key);
684 if (value == null) {
685 // only null adds the value
686 if (value !== undefined)
687 search += key;
688 continue;
689 }
690 // keep null values
691 let values = Array.isArray(value)
692 ? value.map(v => v && encodeQueryProperty(v))
693 : [value && encodeQueryProperty(value)];
694 for (let i = 0; i < values.length; i++) {
695 // only append & with i > 0
696 search += (i ? '&' : '') + key;
697 if (values[i] != null)
698 search += ('=' + values[i]);
699 }
700 }
701 return search;
702}
703/**
704 * Transforms a {@link LocationQueryRaw} into a {@link LocationQuery} by casting
705 * numbers into strings, removing keys with an undefined value and replacing
706 * undefined with null in arrays
707 *
708 * @param query - query object to normalize
709 * @returns a normalized query object
710 */
711function normalizeQuery(query) {
712 const normalizedQuery = {};
713 for (let key in query) {
714 let value = query[key];
715 if (value !== undefined) {
716 normalizedQuery[key] = Array.isArray(value)
717 ? value.map(v => (v == null ? null : '' + v))
718 : value == null
719 ? value
720 : '' + value;
721 }
722 }
723 return normalizedQuery;
724}
725
726function isRouteLocation(route) {
727 return typeof route === 'string' || (route && typeof route === 'object');
728}
729function isRouteName(name) {
730 return typeof name === 'string' || typeof name === 'symbol';
731}
732
733const START_LOCATION_NORMALIZED = vue.markRaw({
734 path: '/',
735 name: undefined,
736 params: {},
737 query: {},
738 hash: '',
739 fullPath: '/',
740 matched: [],
741 meta: {},
742 redirectedFrom: undefined,
743});
744
745(function (NavigationFailureType) {
746 NavigationFailureType[NavigationFailureType["cancelled"] = 3] = "cancelled";
747 NavigationFailureType[NavigationFailureType["aborted"] = 2] = "aborted";
748})(exports.NavigationFailureType || (exports.NavigationFailureType = {}));
749// DEV only debug messages
750const ErrorTypeMessages = {
751 [0 /* MATCHER_NOT_FOUND */]({ location, currentLocation }) {
752 return `No match for\n ${JSON.stringify(location)}${currentLocation
753 ? '\nwhile being at\n' + JSON.stringify(currentLocation)
754 : ''}`;
755 },
756 [1 /* NAVIGATION_GUARD_REDIRECT */]({ from, to, }) {
757 return `Redirected from "${from.fullPath}" to "${stringifyRoute(to)}" via a navigation guard`;
758 },
759 [2 /* NAVIGATION_ABORTED */]({ from, to }) {
760 return `Navigation aborted from "${from.fullPath}" to "${to.fullPath}" via a navigation guard`;
761 },
762 [3 /* NAVIGATION_CANCELLED */]({ from, to }) {
763 return `Navigation cancelled from "${from.fullPath}" to "${to.fullPath}" with a new \`push\` or \`replace\``;
764 },
765};
766function createRouterError(type, params) {
767 {
768 return Object.assign(new Error(ErrorTypeMessages[type](params)), { type }, params);
769 }
770}
771const propertiesToLog = ['params', 'query', 'hash'];
772function stringifyRoute(to) {
773 if (typeof to === 'string')
774 return to;
775 if ('path' in to)
776 return to.path;
777 const location = {};
778 for (const key of propertiesToLog) {
779 if (key in to)
780 location[key] = to[key];
781 }
782 return JSON.stringify(location, null, 2);
783}
784
785// default pattern for a param: non greedy everything but /
786const BASE_PARAM_PATTERN = '[^/]+?';
787const BASE_PATH_PARSER_OPTIONS = {
788 sensitive: false,
789 strict: false,
790 start: true,
791 end: true,
792};
793// Special Regex characters that must be escaped in static tokens
794const REGEX_CHARS_RE = /[.+*?^${}()[\]/\\]/g;
795/**
796 * Creates a path parser from an array of Segments (a segment is an array of Tokens)
797 *
798 * @param segments - array of segments returned by tokenizePath
799 * @param extraOptions - optional options for the regexp
800 * @returns a PathParser
801 */
802function tokensToParser(segments, extraOptions) {
803 const options = {
804 ...BASE_PATH_PARSER_OPTIONS,
805 ...extraOptions,
806 };
807 // the amount of scores is the same as the length of segments except for the root segment "/"
808 let score = [];
809 // the regexp as a string
810 let pattern = options.start ? '^' : '';
811 // extracted keys
812 const keys = [];
813 for (const segment of segments) {
814 // the root segment needs special treatment
815 const segmentScores = segment.length ? [] : [90 /* Root */];
816 // allow trailing slash
817 if (options.strict && !segment.length)
818 pattern += '/';
819 for (let tokenIndex = 0; tokenIndex < segment.length; tokenIndex++) {
820 const token = segment[tokenIndex];
821 // resets the score if we are inside a sub segment /:a-other-:b
822 let subSegmentScore = 40 /* Segment */ +
823 (options.sensitive ? 0.25 /* BonusCaseSensitive */ : 0);
824 if (token.type === 0 /* Static */) {
825 // prepend the slash if we are starting a new segment
826 if (!tokenIndex)
827 pattern += '/';
828 pattern += token.value.replace(REGEX_CHARS_RE, '\\$&');
829 subSegmentScore += 40 /* Static */;
830 }
831 else if (token.type === 1 /* Param */) {
832 const { value, repeatable, optional, regexp } = token;
833 keys.push({
834 name: value,
835 repeatable,
836 optional,
837 });
838 const re = regexp ? regexp : BASE_PARAM_PATTERN;
839 // the user provided a custom regexp /:id(\\d+)
840 if (re !== BASE_PARAM_PATTERN) {
841 subSegmentScore += 10 /* BonusCustomRegExp */;
842 // make sure the regexp is valid before using it
843 try {
844 new RegExp(`(${re})`);
845 }
846 catch (err) {
847 throw new Error(`Invalid custom RegExp for param "${value}" (${re}): ` +
848 err.message);
849 }
850 }
851 // when we repeat we must take care of the repeating leading slash
852 let subPattern = repeatable ? `((?:${re})(?:/(?:${re}))*)` : `(${re})`;
853 // prepend the slash if we are starting a new segment
854 if (!tokenIndex)
855 subPattern = optional ? `(?:/${subPattern})` : '/' + subPattern;
856 if (optional)
857 subPattern += '?';
858 pattern += subPattern;
859 subSegmentScore += 20 /* Dynamic */;
860 if (optional)
861 subSegmentScore += -8 /* BonusOptional */;
862 if (repeatable)
863 subSegmentScore += -20 /* BonusRepeatable */;
864 if (re === '.*')
865 subSegmentScore += -50 /* BonusWildcard */;
866 }
867 segmentScores.push(subSegmentScore);
868 }
869 // an empty array like /home/ -> [[{home}], []]
870 // if (!segment.length) pattern += '/'
871 score.push(segmentScores);
872 }
873 // only apply the strict bonus to the last score
874 if (options.strict && options.end) {
875 const i = score.length - 1;
876 score[i][score[i].length - 1] += 0.7000000000000001 /* BonusStrict */;
877 }
878 // TODO: dev only warn double trailing slash
879 if (!options.strict)
880 pattern += '/?';
881 if (options.end)
882 pattern += '$';
883 // allow paths like /dynamic to only match dynamic or dynamic/... but not dynamic_something_else
884 else if (options.strict)
885 pattern += '(?:/|$)';
886 const re = new RegExp(pattern, options.sensitive ? '' : 'i');
887 function parse(path) {
888 const match = path.match(re);
889 const params = {};
890 if (!match)
891 return null;
892 for (let i = 1; i < match.length; i++) {
893 const value = match[i] || '';
894 const key = keys[i - 1];
895 params[key.name] = value && key.repeatable ? value.split('/') : value;
896 }
897 return params;
898 }
899 function stringify(params) {
900 let path = '';
901 // for optional parameters to allow to be empty
902 let avoidDuplicatedSlash = false;
903 for (const segment of segments) {
904 if (!avoidDuplicatedSlash || path[path.length - 1] !== '/')
905 path += '/';
906 avoidDuplicatedSlash = false;
907 for (const token of segment) {
908 if (token.type === 0 /* Static */) {
909 path += token.value;
910 }
911 else if (token.type === 1 /* Param */) {
912 const { value, repeatable, optional } = token;
913 const param = value in params ? params[value] : '';
914 if (Array.isArray(param) && !repeatable)
915 throw new Error(`Provided param "${value}" is an array but it is not repeatable (* or + modifiers)`);
916 const text = Array.isArray(param) ? param.join('/') : param;
917 if (!text) {
918 // do not append a slash on the next iteration
919 if (optional)
920 avoidDuplicatedSlash = true;
921 else
922 throw new Error(`Missing required param "${value}"`);
923 }
924 path += text;
925 }
926 }
927 }
928 return path;
929 }
930 return {
931 re,
932 score,
933 keys,
934 parse,
935 stringify,
936 };
937}
938/**
939 * Compares an array of numbers as used in PathParser.score and returns a
940 * number. This function can be used to `sort` an array
941 * @param a - first array of numbers
942 * @param b - second array of numbers
943 * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
944 * should be sorted first
945 */
946function compareScoreArray(a, b) {
947 let i = 0;
948 while (i < a.length && i < b.length) {
949 const diff = b[i] - a[i];
950 // only keep going if diff === 0
951 if (diff)
952 return diff;
953 i++;
954 }
955 // if the last subsegment was Static, the shorter segments should be sorted first
956 // otherwise sort the longest segment first
957 if (a.length < b.length) {
958 return a.length === 1 && a[0] === 40 /* Static */ + 40 /* Segment */
959 ? -1
960 : 1;
961 }
962 else if (a.length > b.length) {
963 return b.length === 1 && b[0] === 40 /* Static */ + 40 /* Segment */
964 ? 1
965 : -1;
966 }
967 return 0;
968}
969/**
970 * Compare function that can be used with `sort` to sort an array of PathParser
971 * @param a - first PathParser
972 * @param b - second PathParser
973 * @returns 0 if both are equal, < 0 if a should be sorted first, > 0 if b
974 */
975function comparePathParserScore(a, b) {
976 let i = 0;
977 const aScore = a.score;
978 const bScore = b.score;
979 while (i < aScore.length && i < bScore.length) {
980 const comp = compareScoreArray(aScore[i], bScore[i]);
981 // do not return if both are equal
982 if (comp)
983 return comp;
984 i++;
985 }
986 // if a and b share the same score entries but b has more, sort b first
987 return bScore.length - aScore.length;
988 // this is the ternary version
989 // return aScore.length < bScore.length
990 // ? 1
991 // : aScore.length > bScore.length
992 // ? -1
993 // : 0
994}
995
996const ROOT_TOKEN = {
997 type: 0 /* Static */,
998 value: '',
999};
1000const VALID_PARAM_RE = /[a-zA-Z0-9_]/;
1001function tokenizePath(path) {
1002 if (!path)
1003 return [[]];
1004 if (path === '/')
1005 return [[ROOT_TOKEN]];
1006 // remove the leading slash
1007 if (path[0] !== '/')
1008 throw new Error('A non-empty path must start with "/"');
1009 function crash(message) {
1010 throw new Error(`ERR (${state})/"${buffer}": ${message}`);
1011 }
1012 let state = 0 /* Static */;
1013 let previousState = state;
1014 const tokens = [];
1015 // the segment will always be valid because we get into the initial state
1016 // with the leading /
1017 let segment;
1018 function finalizeSegment() {
1019 if (segment)
1020 tokens.push(segment);
1021 segment = [];
1022 }
1023 // index on the path
1024 let i = 0;
1025 // char at index
1026 let char;
1027 // buffer of the value read
1028 let buffer = '';
1029 // custom regexp for a param
1030 let customRe = '';
1031 function consumeBuffer() {
1032 if (!buffer)
1033 return;
1034 if (state === 0 /* Static */) {
1035 segment.push({
1036 type: 0 /* Static */,
1037 value: buffer,
1038 });
1039 }
1040 else if (state === 1 /* Param */ ||
1041 state === 2 /* ParamRegExp */ ||
1042 state === 3 /* ParamRegExpEnd */) {
1043 if (segment.length > 1 && (char === '*' || char === '+'))
1044 crash(`A repeatable param (${buffer}) must be alone in its segment. eg: '/:ids+.`);
1045 segment.push({
1046 type: 1 /* Param */,
1047 value: buffer,
1048 regexp: customRe,
1049 repeatable: char === '*' || char === '+',
1050 optional: char === '*' || char === '?',
1051 });
1052 }
1053 else {
1054 crash('Invalid state to consume buffer');
1055 }
1056 buffer = '';
1057 }
1058 function addCharToBuffer() {
1059 buffer += char;
1060 }
1061 while (i < path.length) {
1062 char = path[i++];
1063 if (char === '\\' && state !== 2 /* ParamRegExp */) {
1064 previousState = state;
1065 state = 4 /* EscapeNext */;
1066 continue;
1067 }
1068 switch (state) {
1069 case 0 /* Static */:
1070 if (char === '/') {
1071 if (buffer) {
1072 consumeBuffer();
1073 }
1074 finalizeSegment();
1075 }
1076 else if (char === ':') {
1077 consumeBuffer();
1078 state = 1 /* Param */;
1079 // } else if (char === '{') {
1080 // TODO: handle group (or drop it)
1081 // addCharToBuffer()
1082 }
1083 else {
1084 addCharToBuffer();
1085 }
1086 break;
1087 case 4 /* EscapeNext */:
1088 addCharToBuffer();
1089 state = previousState;
1090 break;
1091 case 1 /* Param */:
1092 if (char === '(') {
1093 state = 2 /* ParamRegExp */;
1094 customRe = '';
1095 }
1096 else if (VALID_PARAM_RE.test(char)) {
1097 addCharToBuffer();
1098 }
1099 else {
1100 consumeBuffer();
1101 state = 0 /* Static */;
1102 // go back one character if we were not modifying
1103 if (char !== '*' && char !== '?' && char !== '+')
1104 i--;
1105 }
1106 break;
1107 case 2 /* ParamRegExp */:
1108 if (char === ')') {
1109 // handle the escaped )
1110 if (customRe[customRe.length - 1] == '\\')
1111 customRe = customRe.slice(0, -1) + char;
1112 else
1113 state = 3 /* ParamRegExpEnd */;
1114 }
1115 else {
1116 customRe += char;
1117 }
1118 break;
1119 case 3 /* ParamRegExpEnd */:
1120 // same as finalizing a param
1121 consumeBuffer();
1122 state = 0 /* Static */;
1123 // go back one character if we were not modifying
1124 if (char !== '*' && char !== '?' && char !== '+')
1125 i--;
1126 break;
1127 default:
1128 crash('Unknown state');
1129 break;
1130 }
1131 }
1132 if (state === 2 /* ParamRegExp */)
1133 crash(`Unfinished custom RegExp for param "${buffer}"`);
1134 consumeBuffer();
1135 finalizeSegment();
1136 return tokens;
1137}
1138
1139function createRouteRecordMatcher(record, parent, options) {
1140 const parser = tokensToParser(tokenizePath(record.path), options);
1141 const matcher = {
1142 ...parser,
1143 record,
1144 parent,
1145 // these needs to be populated by the parent
1146 children: [],
1147 alias: [],
1148 };
1149 if (parent) {
1150 // both are aliases or both are not aliases
1151 // we don't want to mix them because the order is used when
1152 // passing originalRecord in Matcher.addRoute
1153 if (!matcher.record.aliasOf === !parent.record.aliasOf)
1154 parent.children.push(matcher);
1155 // else TODO: save alias children to be able to remove them
1156 }
1157 return matcher;
1158}
1159
1160let noop = () => { };
1161function createRouterMatcher(routes, globalOptions) {
1162 // normalized ordered array of matchers
1163 const matchers = [];
1164 const matcherMap = new Map();
1165 function getRecordMatcher(name) {
1166 return matcherMap.get(name);
1167 }
1168 // TODO: add routes to children of parent
1169 function addRoute(record, parent, originalRecord) {
1170 let mainNormalizedRecord = normalizeRouteRecord(record);
1171 // we might be the child of an alias
1172 mainNormalizedRecord.aliasOf = originalRecord && originalRecord.record;
1173 const options = { ...globalOptions, ...record.options };
1174 // generate an array of records to correctly handle aliases
1175 const normalizedRecords = [
1176 mainNormalizedRecord,
1177 ];
1178 if ('alias' in record) {
1179 const aliases = typeof record.alias === 'string' ? [record.alias] : record.alias;
1180 for (const alias of aliases) {
1181 normalizedRecords.push({
1182 ...mainNormalizedRecord,
1183 // this allows us to hold a copy of the `components` option
1184 // so that async components cache is hold on the original record
1185 components: originalRecord
1186 ? originalRecord.record.components
1187 : mainNormalizedRecord.components,
1188 path: alias,
1189 // we might be the child of an alias
1190 aliasOf: originalRecord
1191 ? originalRecord.record
1192 : mainNormalizedRecord,
1193 });
1194 }
1195 }
1196 let matcher;
1197 let originalMatcher;
1198 for (const normalizedRecord of normalizedRecords) {
1199 let { path } = normalizedRecord;
1200 // Build up the path for nested routes if the child isn't an absolute
1201 // route. Only add the / delimiter if the child path isn't empty and if the
1202 // parent path doesn't have a trailing slash
1203 if (parent && path[0] !== '/') {
1204 let parentPath = parent.record.path;
1205 let connectingSlash = parentPath[parentPath.length - 1] === '/' ? '' : '/';
1206 normalizedRecord.path =
1207 parent.record.path + (path && connectingSlash + path);
1208 }
1209 // create the object before hand so it can be passed to children
1210 matcher = createRouteRecordMatcher(normalizedRecord, parent, options);
1211 // if we are an alias we must tell the original record that we exist
1212 // so we can be removed
1213 if (originalRecord) {
1214 originalRecord.alias.push(matcher);
1215 }
1216 else {
1217 // otherwise, the first record is the original and others are aliases
1218 originalMatcher = originalMatcher || matcher;
1219 if (originalMatcher !== matcher)
1220 originalMatcher.alias.push(matcher);
1221 }
1222 // only non redirect records have children
1223 if ('children' in mainNormalizedRecord) {
1224 let children = mainNormalizedRecord.children;
1225 for (let i = 0; i < children.length; i++) {
1226 addRoute(children[i], matcher, originalRecord && originalRecord.children[i]);
1227 }
1228 }
1229 // if there was no original record, then the first one was not an alias and all
1230 // other alias (if any) need to reference this record when adding children
1231 originalRecord = originalRecord || matcher;
1232 insertMatcher(matcher);
1233 }
1234 return originalMatcher
1235 ? () => {
1236 // since other matchers are aliases, they should be removed by the original matcher
1237 removeRoute(originalMatcher);
1238 }
1239 : noop;
1240 }
1241 function removeRoute(matcherRef) {
1242 if (isRouteName(matcherRef)) {
1243 const matcher = matcherMap.get(matcherRef);
1244 if (matcher) {
1245 matcherMap.delete(matcherRef);
1246 matchers.splice(matchers.indexOf(matcher), 1);
1247 matcher.children.forEach(removeRoute);
1248 matcher.alias.forEach(removeRoute);
1249 }
1250 }
1251 else {
1252 let index = matchers.indexOf(matcherRef);
1253 if (index > -1) {
1254 matchers.splice(index, 1);
1255 if (matcherRef.record.name)
1256 matcherMap.delete(matcherRef.record.name);
1257 matcherRef.children.forEach(removeRoute);
1258 matcherRef.alias.forEach(removeRoute);
1259 }
1260 }
1261 }
1262 function getRoutes() {
1263 return matchers;
1264 }
1265 function insertMatcher(matcher) {
1266 let i = 0;
1267 // console.log('i is', { i })
1268 while (i < matchers.length &&
1269 comparePathParserScore(matcher, matchers[i]) >= 0)
1270 i++;
1271 // console.log('END i is', { i })
1272 // while (i < matchers.length && matcher.score <= matchers[i].score) i++
1273 matchers.splice(i, 0, matcher);
1274 // only add the original record to the name map
1275 if (matcher.record.name && !isAliasRecord(matcher))
1276 matcherMap.set(matcher.record.name, matcher);
1277 }
1278 /**
1279 * Resolves a location. Gives access to the route record that corresponds to the actual path as well as filling the corresponding params objects
1280 *
1281 * @param location - MatcherLocationRaw to resolve to a url
1282 * @param currentLocation - MatcherLocation of the current location
1283 */
1284 function resolve(location, currentLocation) {
1285 let matcher;
1286 let params = {};
1287 let path;
1288 let name;
1289 if ('name' in location && location.name) {
1290 matcher = matcherMap.get(location.name);
1291 if (!matcher)
1292 throw createRouterError(0 /* MATCHER_NOT_FOUND */, {
1293 location,
1294 });
1295 name = matcher.record.name;
1296 // TODO: merge params with current location. Should this be done by name. I think there should be some kind of relationship between the records like children of a parent should keep parent props but not the rest
1297 // needs an RFC if breaking change
1298 params = location.params || currentLocation.params;
1299 // throws if cannot be stringified
1300 path = matcher.stringify(params);
1301 }
1302 else if ('path' in location) {
1303 matcher = matchers.find(m => m.re.test(location.path));
1304 // matcher should have a value after the loop
1305 // no need to resolve the path with the matcher as it was provided
1306 // this also allows the user to control the encoding
1307 path = location.path;
1308 if (matcher) {
1309 // TODO: dev warning of unused params if provided
1310 params = matcher.parse(location.path);
1311 name = matcher.record.name;
1312 }
1313 // location is a relative path
1314 }
1315 else {
1316 // match by name or path of current route
1317 matcher = currentLocation.name
1318 ? matcherMap.get(currentLocation.name)
1319 : matchers.find(m => m.re.test(currentLocation.path));
1320 if (!matcher)
1321 throw createRouterError(0 /* MATCHER_NOT_FOUND */, {
1322 location,
1323 currentLocation,
1324 });
1325 name = matcher.record.name;
1326 params = location.params || currentLocation.params;
1327 path = matcher.stringify(params);
1328 }
1329 const matched = [];
1330 let parentMatcher = matcher;
1331 while (parentMatcher) {
1332 // reversed order so parents are at the beginning
1333 // TODO: check resolving child routes by path when parent has an alias
1334 matched.unshift(parentMatcher.record);
1335 parentMatcher = parentMatcher.parent;
1336 }
1337 return {
1338 name,
1339 path,
1340 params,
1341 matched,
1342 meta: mergeMetaFields(matched),
1343 };
1344 }
1345 // add initial routes
1346 routes.forEach(route => addRoute(route));
1347 return { addRoute, resolve, removeRoute, getRoutes, getRecordMatcher };
1348}
1349/**
1350 * Normalizes a RouteRecordRaw. Transforms the `redirect` option into a `beforeEnter`
1351 * @param record
1352 * @returns the normalized version
1353 */
1354function normalizeRouteRecord(record) {
1355 const commonInitialValues = {
1356 path: record.path,
1357 name: record.name,
1358 meta: record.meta || {},
1359 aliasOf: undefined,
1360 components: {},
1361 };
1362 if ('redirect' in record) {
1363 return {
1364 ...commonInitialValues,
1365 redirect: record.redirect,
1366 };
1367 }
1368 else {
1369 return {
1370 ...commonInitialValues,
1371 beforeEnter: record.beforeEnter,
1372 props: record.props || false,
1373 children: record.children || [],
1374 instances: {},
1375 leaveGuards: [],
1376 components: 'components' in record
1377 ? record.components
1378 : { default: record.component },
1379 };
1380 }
1381}
1382/**
1383 * Checks if a record or any of its parent is an alias
1384 * @param record
1385 */
1386function isAliasRecord(record) {
1387 while (record) {
1388 if (record.record.aliasOf)
1389 return true;
1390 record = record.parent;
1391 }
1392 return false;
1393}
1394/**
1395 * Merge meta fields of an array of records
1396 *
1397 * @param matched array of matched records
1398 */
1399function mergeMetaFields(matched) {
1400 return matched.reduce((meta, record) => ({
1401 ...meta,
1402 ...record.meta,
1403 }), {});
1404}
1405
1406/**
1407 * Create a list of callbacks that can be reset. Used to create before and after navigation guards list
1408 */
1409function useCallbacks() {
1410 let handlers = [];
1411 function add(handler) {
1412 handlers.push(handler);
1413 return () => {
1414 const i = handlers.indexOf(handler);
1415 if (i > -1)
1416 handlers.splice(i, 1);
1417 };
1418 }
1419 function reset() {
1420 handlers = [];
1421 }
1422 return {
1423 add,
1424 list: () => handlers,
1425 reset,
1426 };
1427}
1428
1429// TODO: we could allow currentRoute as a prop to expose `isActive` and
1430// `isExactActive` behavior should go through an RFC
1431function useLink(props) {
1432 const router = vue.inject(routerKey);
1433 const currentRoute = vue.inject(routeLocationKey);
1434 const route = vue.computed(() => router.resolve(vue.unref(props.to)));
1435 const activeRecordIndex = vue.computed(() => {
1436 // TODO: handle children with empty path: they should relate to their parent
1437 const currentMatched = route.value.matched[route.value.matched.length - 1];
1438 if (!currentMatched)
1439 return -1;
1440 return currentRoute.matched.findIndex(isSameRouteRecord.bind(null, currentMatched));
1441 });
1442 const isActive = vue.computed(() => activeRecordIndex.value > -1 &&
1443 includesParams(currentRoute.params, route.value.params));
1444 const isExactActive = vue.computed(() => activeRecordIndex.value > -1 &&
1445 activeRecordIndex.value === currentRoute.matched.length - 1 &&
1446 isSameLocationObject(currentRoute.params, route.value.params));
1447 // TODO: handle replace prop
1448 // const method = unref(rep)
1449 function navigate(e = {}) {
1450 // TODO: handle navigate with empty parameters for scoped slot and composition api
1451 if (guardEvent(e))
1452 router.push(route.value);
1453 }
1454 return {
1455 route,
1456 href: vue.computed(() => route.value.href),
1457 isActive,
1458 isExactActive,
1459 navigate,
1460 };
1461}
1462const Link = vue.defineComponent({
1463 name: 'RouterLink',
1464 props: {
1465 to: {
1466 type: [String, Object],
1467 required: true,
1468 },
1469 activeClass: {
1470 type: String,
1471 default: 'router-link-active',
1472 },
1473 exactActiveClass: {
1474 type: String,
1475 default: 'router-link-exact-active',
1476 },
1477 custom: Boolean,
1478 },
1479 setup(props, { slots, attrs }) {
1480 const link = vue.reactive(useLink(props));
1481 const elClass = vue.computed(() => ({
1482 [props.activeClass]: link.isActive,
1483 [props.exactActiveClass]: link.isExactActive,
1484 }));
1485 return () => {
1486 const children = slots.default && slots.default(link);
1487 return props.custom
1488 ? children
1489 : vue.h('a', {
1490 'aria-current': link.isExactActive ? 'page' : null,
1491 onClick: link.navigate,
1492 href: link.href,
1493 ...attrs,
1494 class: elClass.value,
1495 }, children);
1496 };
1497 },
1498});
1499function guardEvent(e) {
1500 // don't redirect with control keys
1501 if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey)
1502 return;
1503 // don't redirect when preventDefault called
1504 if (e.defaultPrevented)
1505 return;
1506 // don't redirect on right click
1507 if (e.button !== undefined && e.button !== 0)
1508 return;
1509 // don't redirect if `target="_blank"`
1510 // @ts-ignore getAttribute does exist
1511 if (e.currentTarget && e.currentTarget.getAttribute) {
1512 // @ts-ignore getAttribute exists
1513 const target = e.currentTarget.getAttribute('target');
1514 if (/\b_blank\b/i.test(target))
1515 return;
1516 }
1517 // this may be a Weex event which doesn't have this method
1518 if (e.preventDefault)
1519 e.preventDefault();
1520 return true;
1521}
1522function includesParams(outer, inner) {
1523 for (let key in inner) {
1524 let innerValue = inner[key];
1525 let outerValue = outer[key];
1526 if (typeof innerValue === 'string') {
1527 if (innerValue !== outerValue)
1528 return false;
1529 }
1530 else {
1531 if (!Array.isArray(outerValue) ||
1532 outerValue.length !== innerValue.length ||
1533 innerValue.some((value, i) => value !== outerValue[i]))
1534 return false;
1535 }
1536 }
1537 return true;
1538}
1539
1540const View = vue.defineComponent({
1541 name: 'RouterView',
1542 props: {
1543 name: {
1544 type: String,
1545 default: 'default',
1546 },
1547 route: Object,
1548 },
1549 setup(props, { attrs, slots }) {
1550 const realRoute = vue.inject(routeLocationKey);
1551 const route = vue.computed(() => props.route || realRoute);
1552 const depth = vue.inject(viewDepthKey, 0);
1553 vue.provide(viewDepthKey, depth + 1);
1554 const matchedRoute = vue.computed(() => route.value.matched[depth]);
1555 const ViewComponent = vue.computed(() => matchedRoute.value && matchedRoute.value.components[props.name]);
1556 const propsData = vue.computed(() => {
1557 // propsData only gets called if ViewComponent.value exists and it depends on matchedRoute.value
1558 const { props } = matchedRoute.value;
1559 if (!props)
1560 return {};
1561 if (props === true)
1562 return route.value.params;
1563 return typeof props === 'object' ? props : props(route.value);
1564 });
1565 vue.provide(matchedRouteKey, matchedRoute);
1566 const viewRef = vue.ref();
1567 function onVnodeMounted() {
1568 // if we mount, there is a matched record
1569 matchedRoute.value.instances[props.name] = viewRef.value;
1570 // TODO: trigger beforeRouteEnter hooks
1571 // TODO: watch name to update the instance record
1572 }
1573 return () => {
1574 // we nee the value at the time we render because when we unmount, we
1575 // navigated to a different location so the value is different
1576 const currentMatched = matchedRoute.value;
1577 function onVnodeUnmounted() {
1578 if (currentMatched) {
1579 // remove the instance reference to prevent leak
1580 currentMatched.instances[props.name] = null;
1581 }
1582 }
1583 let Component = ViewComponent.value;
1584 const componentProps = {
1585 ...(Component && propsData.value),
1586 ...attrs,
1587 onVnodeMounted,
1588 onVnodeUnmounted,
1589 ref: viewRef,
1590 };
1591 const children = Component &&
1592 slots.default &&
1593 slots.default({ Component, props: componentProps });
1594 return children
1595 ? children
1596 : Component
1597 ? vue.h(Component, componentProps)
1598 : null;
1599 };
1600 },
1601});
1602
1603function onBeforeRouteLeave(leaveGuard) {
1604 const instance = vue.getCurrentInstance();
1605 if (!instance) {
1606
1607 vue.warn('onRouteLeave must be called at the top of a setup function');
1608 return;
1609 }
1610 const activeRecord = vue.inject(matchedRouteKey, {}).value;
1611 if (!activeRecord) {
1612
1613 vue.warn('onRouteLeave must be called at the top of a setup function');
1614 return;
1615 }
1616 activeRecord.leaveGuards.push(
1617 // @ts-ignore do we even want to allow that? Passing the context in a composition api hook doesn't make sense
1618 leaveGuard.bind(instance.proxy));
1619}
1620function guardToPromiseFn(guard, to, from, instance) {
1621 return () => new Promise((resolve, reject) => {
1622 const next = (valid) => {
1623 if (valid === false)
1624 reject(createRouterError(2 /* NAVIGATION_ABORTED */, {
1625 from,
1626 to,
1627 }));
1628 else if (valid instanceof Error) {
1629 reject(valid);
1630 }
1631 else if (isRouteLocation(valid)) {
1632 reject(createRouterError(1 /* NAVIGATION_GUARD_REDIRECT */, {
1633 from: to,
1634 to: valid,
1635 }));
1636 }
1637 else {
1638 // TODO: call the in component enter callbacks. Maybe somewhere else
1639 // record && record.enterCallbacks.push(valid)
1640 resolve();
1641 }
1642 };
1643 // wrapping with Promise.resolve allows it to work with both async and sync guards
1644 Promise.resolve(guard.call(instance, to, from, next)).catch(err => reject(err));
1645 });
1646}
1647function extractComponentsGuards(matched, guardType, to, from) {
1648 const guards = [];
1649 for (const record of matched) {
1650 for (const name in record.components) {
1651 const rawComponent = record.components[name];
1652 if (typeof rawComponent === 'function') {
1653 // start requesting the chunk already
1654 const componentPromise = rawComponent().catch(() => null);
1655 guards.push(async () => {
1656 const resolved = await componentPromise;
1657 if (!resolved)
1658 throw new Error('TODO: error while fetching');
1659 const resolvedComponent = isESModule(resolved)
1660 ? resolved.default
1661 : resolved;
1662 // replace the function with the resolved component
1663 record.components[name] = resolvedComponent;
1664 // @ts-ignore: the options types are not propagated to Component
1665 const guard = resolvedComponent[guardType];
1666 return (
1667 // @ts-ignore: the guards matched the instance type
1668 guard && guardToPromiseFn(guard, to, from, record.instances[name])());
1669 });
1670 }
1671 else {
1672 const guard = rawComponent[guardType];
1673 guard &&
1674 // @ts-ignore: the guards matched the instance type
1675 guards.push(guardToPromiseFn(guard, to, from, record.instances[name]));
1676 }
1677 }
1678 }
1679 return guards;
1680}
1681
1682function createRouter({ history, routes, scrollBehavior, parseQuery: parseQuery$1 = parseQuery, stringifyQuery: stringifyQuery$1 = stringifyQuery, }) {
1683 const matcher = createRouterMatcher(routes, {});
1684 const beforeGuards = useCallbacks();
1685 const afterGuards = useCallbacks();
1686 const currentRoute = vue.ref(START_LOCATION_NORMALIZED);
1687 let pendingLocation = START_LOCATION_NORMALIZED;
1688 if (isBrowser && 'scrollRestoration' in window.history) {
1689 window.history.scrollRestoration = 'manual';
1690 }
1691 const encodeParams = applyToParams.bind(null, encodeParam);
1692 const decodeParams = applyToParams.bind(null, decode);
1693 function addRoute(parentOrRoute, route) {
1694 let parent;
1695 let record;
1696 if (isRouteName(parentOrRoute)) {
1697 parent = matcher.getRecordMatcher(parentOrRoute);
1698 record = route;
1699 }
1700 else {
1701 record = parentOrRoute;
1702 }
1703 return matcher.addRoute(record, parent);
1704 }
1705 function removeRoute(name) {
1706 let recordMatcher = matcher.getRecordMatcher(name);
1707 if (recordMatcher) {
1708 matcher.removeRoute(recordMatcher);
1709 }
1710 else {
1711 vue.warn(`Cannot remove non-existent route "${String(name)}"`);
1712 }
1713 }
1714 function getRoutes() {
1715 return matcher.getRoutes().map(routeMatcher => routeMatcher.record);
1716 }
1717 function hasRoute(name) {
1718 return !!matcher.getRecordMatcher(name);
1719 }
1720 function resolve(location, currentLocation) {
1721 // const objectLocation = routerLocationAsObject(location)
1722 currentLocation = currentLocation || currentRoute.value;
1723 if (typeof location === 'string') {
1724 let locationNormalized = parseURL(parseQuery$1, location);
1725 let matchedRoute = matcher.resolve({ path: locationNormalized.path }, currentLocation);
1726 return {
1727 // fullPath: locationNormalized.fullPath,
1728 // query: locationNormalized.query,
1729 // hash: locationNormalized.hash,
1730 ...locationNormalized,
1731 ...matchedRoute,
1732 // path: matchedRoute.path,
1733 // name: matchedRoute.name,
1734 // meta: matchedRoute.meta,
1735 // matched: matchedRoute.matched,
1736 params: decodeParams(matchedRoute.params),
1737 redirectedFrom: undefined,
1738 href: history.base + locationNormalized.fullPath,
1739 };
1740 }
1741 let matchedRoute =
1742 // for same reason TS thinks location.params can be undefined
1743 matcher.resolve('params' in location
1744 ? { ...location, params: encodeParams(location.params) }
1745 : location, currentLocation);
1746 // put back the unencoded params as given by the user (avoid the cost of decoding them)
1747 // TODO: normalize params if we accept numbers as raw values
1748 matchedRoute.params =
1749 'params' in location
1750 ? location.params
1751 : decodeParams(matchedRoute.params);
1752 const fullPath = stringifyURL(stringifyQuery$1, {
1753 ...location,
1754 path: matchedRoute.path,
1755 });
1756 return {
1757 fullPath,
1758 hash: location.hash || '',
1759 query: normalizeQuery(location.query),
1760 ...matchedRoute,
1761 redirectedFrom: undefined,
1762 href: history.base + fullPath,
1763 };
1764 }
1765 function push(to) {
1766 return pushWithRedirect(to, undefined);
1767 }
1768 function replace(to) {
1769 const location = typeof to === 'string' ? { path: to } : to;
1770 return push({ ...location, replace: true });
1771 }
1772 async function pushWithRedirect(to, redirectedFrom) {
1773 const targetLocation = (pendingLocation = resolve(to));
1774 const from = currentRoute.value;
1775 const data = to.state;
1776 // @ts-ignore: no need to check the string as force do not exist on a string
1777 const force = to.force;
1778 if (!force && isSameRouteLocation(from, targetLocation))
1779 return;
1780 const lastMatched = targetLocation.matched[targetLocation.matched.length - 1];
1781 if (lastMatched && 'redirect' in lastMatched) {
1782 const { redirect } = lastMatched;
1783 return pushWithRedirect(typeof redirect === 'function' ? redirect(targetLocation) : redirect,
1784 // keep original redirectedFrom if it exists
1785 redirectedFrom || targetLocation);
1786 }
1787 // if it was a redirect we already called `pushWithRedirect` above
1788 const toLocation = targetLocation;
1789 toLocation.redirectedFrom = redirectedFrom;
1790 let failure;
1791 // trigger all guards, throw if navigation is rejected
1792 try {
1793 await navigate(toLocation, from);
1794 }
1795 catch (error) {
1796 // a more recent navigation took place
1797 if (pendingLocation !== toLocation) {
1798 failure = createRouterError(3 /* NAVIGATION_CANCELLED */, {
1799 from,
1800 to: toLocation,
1801 });
1802 }
1803 else if (error.type === 2 /* NAVIGATION_ABORTED */) {
1804 failure = error;
1805 }
1806 else if (error.type === 1 /* NAVIGATION_GUARD_REDIRECT */) {
1807 // preserve the original redirectedFrom if any
1808 return pushWithRedirect(error.to, redirectedFrom || toLocation);
1809 }
1810 else {
1811 // unknown error, throws
1812 triggerError(error, true);
1813 }
1814 }
1815 // if we fail we don't finalize the navigation
1816 failure =
1817 failure ||
1818 finalizeNavigation(toLocation, from, true,
1819 // RouteLocationNormalized will give undefined
1820 to.replace === true, data);
1821 triggerAfterEach(toLocation, from, failure);
1822 return failure;
1823 }
1824 async function navigate(to, from) {
1825 let guards;
1826 // all components here have been resolved once because we are leaving
1827 // TODO: refactor both together
1828 guards = extractComponentsGuards(from.matched.filter(record => to.matched.indexOf(record) < 0).reverse(), 'beforeRouteLeave', to, from);
1829 const [leavingRecords,] = extractChangingRecords(to, from);
1830 for (const record of leavingRecords) {
1831 for (const guard of record.leaveGuards) {
1832 guards.push(guardToPromiseFn(guard, to, from));
1833 }
1834 }
1835 // run the queue of per route beforeRouteLeave guards
1836 await runGuardQueue(guards);
1837 // check global guards beforeEach
1838 guards = [];
1839 for (const guard of beforeGuards.list()) {
1840 guards.push(guardToPromiseFn(guard, to, from));
1841 }
1842 await runGuardQueue(guards);
1843 // check in components beforeRouteUpdate
1844 guards = extractComponentsGuards(to.matched.filter(record => from.matched.indexOf(record) > -1), 'beforeRouteUpdate', to, from);
1845 // run the queue of per route beforeEnter guards
1846 await runGuardQueue(guards);
1847 // check the route beforeEnter
1848 guards = [];
1849 for (const record of to.matched) {
1850 // do not trigger beforeEnter on reused views
1851 if (record.beforeEnter && from.matched.indexOf(record) < 0) {
1852 if (Array.isArray(record.beforeEnter)) {
1853 for (const beforeEnter of record.beforeEnter)
1854 guards.push(guardToPromiseFn(beforeEnter, to, from));
1855 }
1856 else {
1857 guards.push(guardToPromiseFn(record.beforeEnter, to, from));
1858 }
1859 }
1860 }
1861 // run the queue of per route beforeEnter guards
1862 await runGuardQueue(guards);
1863 // TODO: at this point to.matched is normalized and does not contain any () => Promise<Component>
1864 // check in-component beforeRouteEnter
1865 guards = extractComponentsGuards(
1866 // the type doesn't matter as we are comparing an object per reference
1867 to.matched.filter(record => from.matched.indexOf(record) < 0), 'beforeRouteEnter', to, from);
1868 // run the queue of per route beforeEnter guards
1869 await runGuardQueue(guards);
1870 }
1871 function triggerAfterEach(to, from, failure) {
1872 // navigation is confirmed, call afterGuards
1873 // TODO: wrap with error handlers
1874 for (const guard of afterGuards.list())
1875 guard(to, from, failure);
1876 }
1877 /**
1878 * - Cleans up any navigation guards
1879 * - Changes the url if necessary
1880 * - Calls the scrollBehavior
1881 */
1882 function finalizeNavigation(toLocation, from, isPush, replace, data) {
1883 // a more recent navigation took place
1884 if (pendingLocation !== toLocation) {
1885 return createRouterError(3 /* NAVIGATION_CANCELLED */, {
1886 from,
1887 to: toLocation,
1888 });
1889 }
1890 const [leavingRecords] = extractChangingRecords(toLocation, from);
1891 for (const record of leavingRecords) {
1892 // remove registered guards from removed matched records
1893 record.leaveGuards = [];
1894 // free the references
1895 // TODO: add tests
1896 record.instances = {};
1897 }
1898 // only consider as push if it's not the first navigation
1899 const isFirstNavigation = from === START_LOCATION_NORMALIZED;
1900 // change URL only if the user did a push/replace and if it's not the initial navigation because
1901 // it's just reflecting the url
1902 if (isPush) {
1903 if (replace || isFirstNavigation)
1904 history.replace(toLocation, data);
1905 else
1906 history.push(toLocation, data);
1907 }
1908 // accept current navigation
1909 currentRoute.value = vue.markRaw(toLocation);
1910 // TODO: this doesn't work on first load. Moving it to RouterView could allow automatically handling transitions too maybe
1911 // TODO: refactor with a state getter
1912 const state = isPush || !isBrowser ? {} : window.history.state;
1913 const savedScroll = getSavedScroll(getScrollKey(toLocation.fullPath, 0));
1914 handleScroll(toLocation, from, savedScroll || (state && state.scroll)).catch(err => triggerError(err));
1915 markAsReady();
1916 }
1917 // attach listener to history to trigger navigations
1918 history.listen(async (to, _from, info) => {
1919 // TODO: try catch to correctly log the matcher error
1920 // cannot be a redirect route because it was in history
1921 const toLocation = resolve(to.fullPath);
1922 pendingLocation = toLocation;
1923 const from = currentRoute.value;
1924 saveScrollOnLeave(getScrollKey(from.fullPath, info.distance));
1925 let failure;
1926 try {
1927 // TODO: refactor using then/catch because no need for async/await + try catch
1928 await navigate(toLocation, from);
1929 }
1930 catch (error) {
1931 // a more recent navigation took place
1932 if (pendingLocation !== toLocation) {
1933 failure = createRouterError(3 /* NAVIGATION_CANCELLED */, {
1934 from,
1935 to: toLocation,
1936 });
1937 }
1938 else if (error.type === 2 /* NAVIGATION_ABORTED */) {
1939 failure = error;
1940 }
1941 else if (error.type === 1 /* NAVIGATION_GUARD_REDIRECT */) {
1942 history.go(-info.distance, false);
1943 // the error is already handled by router.push we just want to avoid
1944 // logging the error
1945 return pushWithRedirect(error.to, toLocation).catch(() => { });
1946 }
1947 else {
1948 // TODO: test on different browsers ensure consistent behavior
1949 history.go(-info.distance, false);
1950 // unrecognized error, transfer to the global handler
1951 return triggerError(error);
1952 }
1953 }
1954 failure =
1955 failure ||
1956 finalizeNavigation(
1957 // after navigation, all matched components are resolved
1958 toLocation, from, false);
1959 // revert the navigation
1960 if (failure)
1961 history.go(-info.distance, false);
1962 triggerAfterEach(toLocation, from, failure);
1963 });
1964 // Initialization and Errors
1965 let readyHandlers = useCallbacks();
1966 let errorHandlers = useCallbacks();
1967 let ready;
1968 /**
1969 * Trigger errorHandlers added via onError and throws the error as well
1970 * @param error - error to throw
1971 * @param shouldThrow - defaults to false. Pass true rethrow the error
1972 * @returns the error (unless shouldThrow is true)
1973 */
1974 function triggerError(error, shouldThrow = false) {
1975 markAsReady(error);
1976 errorHandlers.list().forEach(handler => handler(error));
1977 if (shouldThrow)
1978 throw error;
1979 }
1980 /**
1981 * Returns a Promise that resolves or reject when the router has finished its
1982 * initial navigation. This will be automatic on client but requires an
1983 * explicit `router.push` call on the server. This behavior can change
1984 * depending on the history implementation used e.g. the defaults history
1985 * implementation (client only) triggers this automatically but the memory one
1986 * (should be used on server) doesn't
1987 */
1988 function isReady() {
1989 if (ready && currentRoute.value !== START_LOCATION_NORMALIZED)
1990 return Promise.resolve();
1991 return new Promise((resolve, reject) => {
1992 readyHandlers.add([resolve, reject]);
1993 });
1994 }
1995 /**
1996 * Mark the router as ready, resolving the promised returned by isReady(). Can
1997 * only be called once, otherwise does nothing.
1998 * @param err - optional error
1999 */
2000 function markAsReady(err) {
2001 if (ready)
2002 return;
2003 ready = true;
2004 readyHandlers
2005 .list()
2006 .forEach(([resolve, reject]) => (err ? reject(err) : resolve()));
2007 readyHandlers.reset();
2008 }
2009 // Scroll behavior
2010 async function handleScroll(to, from, scrollPosition) {
2011 if (!scrollBehavior)
2012 return;
2013 await vue.nextTick();
2014 const position = await scrollBehavior(to, from, scrollPosition || null);
2015 position && scrollToPosition(position);
2016 }
2017 const router = {
2018 currentRoute,
2019 addRoute,
2020 removeRoute,
2021 hasRoute,
2022 getRoutes,
2023 push,
2024 replace,
2025 resolve,
2026 beforeEach: beforeGuards.add,
2027 afterEach: afterGuards.add,
2028 onError: errorHandlers.add,
2029 isReady,
2030 history,
2031 install(app) {
2032 applyRouterPlugin(app, this);
2033 },
2034 };
2035 return router;
2036}
2037function applyRouterPlugin(app, router) {
2038 app.component('RouterLink', Link);
2039 app.component('RouterView', View);
2040 // TODO: add tests
2041 app.config.globalProperties.$router = router;
2042 Object.defineProperty(app.config.globalProperties, '$route', {
2043 get: () => router.currentRoute.value,
2044 });
2045 let started = false;
2046 // TODO: can we use something that isn't a mixin? Like adding an onMount hook here
2047 if (isBrowser) {
2048 app.mixin({
2049 beforeCreate() {
2050 if (!started) {
2051 // this initial navigation is only necessary on client, on server it doesn't make sense
2052 // because it will create an extra unnecessary navigation and could lead to problems
2053 router.push(router.history.location.fullPath).catch(err => {
2054 console.error('Unhandled error when starting the router', err);
2055 });
2056 started = true;
2057 }
2058 },
2059 });
2060 }
2061 const reactiveRoute = {};
2062 for (let key in START_LOCATION_NORMALIZED) {
2063 // @ts-ignore: the key matches
2064 reactiveRoute[key] = vue.computed(() => router.currentRoute.value[key]);
2065 }
2066 app.provide(routerKey, router);
2067 app.provide(routeLocationKey, vue.reactive(reactiveRoute));
2068 // TODO: merge strats for beforeRoute hooks
2069}
2070async function runGuardQueue(guards) {
2071 for (const guard of guards) {
2072 await guard();
2073 }
2074}
2075function extractChangingRecords(to, from) {
2076 const leavingRecords = [];
2077 const updatingRecords = [];
2078 const enteringRecords = [];
2079 // TODO: could be optimized with one single for loop
2080 for (const record of from.matched) {
2081 if (to.matched.indexOf(record) < 0)
2082 leavingRecords.push(record);
2083 else
2084 updatingRecords.push(record);
2085 }
2086 for (const record of to.matched) {
2087 // the type doesn't matter because we are comparing per reference
2088 if (from.matched.indexOf(record) < 0)
2089 enteringRecords.push(record);
2090 }
2091 return [leavingRecords, updatingRecords, enteringRecords];
2092}
2093// TODO: move to utils and test
2094function isSameRouteLocation(a, b) {
2095 let aLastIndex = a.matched.length - 1;
2096 let bLastIndex = b.matched.length - 1;
2097 return (aLastIndex > -1 &&
2098 aLastIndex === bLastIndex &&
2099 isSameRouteRecord(a.matched[aLastIndex], b.matched[bLastIndex]) &&
2100 isSameLocationObject(a.params, b.params) &&
2101 isSameLocationObject(a.query, b.query) &&
2102 a.hash === b.hash);
2103}
2104
2105function useRouter() {
2106 return vue.inject(routerKey);
2107}
2108function useRoute() {
2109 return vue.inject(routeLocationKey);
2110}
2111
2112exports.Link = Link;
2113exports.START_LOCATION = START_LOCATION_NORMALIZED;
2114exports.View = View;
2115exports.createMemoryHistory = createMemoryHistory;
2116exports.createRouter = createRouter;
2117exports.createWebHashHistory = createWebHashHistory;
2118exports.createWebHistory = createWebHistory;
2119exports.onBeforeRouteLeave = onBeforeRouteLeave;
2120exports.parseQuery = parseQuery;
2121exports.stringifyQuery = stringifyQuery;
2122exports.useLink = useLink;
2123exports.useRoute = useRoute;
2124exports.useRouter = useRouter;
2125//# sourceMappingURL=vue-router.cjs.js.map