From b3d2a86c253b3cc086753228d35e68b9a6e2e06d Mon Sep 17 00:00:00 2001 From: Dario Nieuwenhuis Date: Tue, 9 Jul 2019 13:29:57 +0200 Subject: [PATCH] Add support for layered routing. --- dist/vue-router.common.js | 1045 +++++++++++++++++----------- dist/vue-router.esm.browser.js | 995 ++++++++++++++++---------- dist/vue-router.esm.browser.min.js | 2 +- dist/vue-router.esm.js | 1045 +++++++++++++++++----------- dist/vue-router.js | 1045 +++++++++++++++++----------- dist/vue-router.min.js | 2 +- src/components/link.js | 18 +- src/components/view.js | 14 +- src/history/base.js | 114 ++- src/history/html5.js | 57 +- src/index.js | 54 +- src/install.js | 24 +- src/util/push-state.js | 6 +- 13 files changed, 2761 insertions(+), 1660 deletions(-) diff --git a/dist/vue-router.common.js b/dist/vue-router.common.js index a97bfb988..06703fb62 100644 --- a/dist/vue-router.common.js +++ b/dist/vue-router.common.js @@ -23,6 +23,10 @@ function isError (err) { return Object.prototype.toString.call(err).indexOf('Error') > -1 } +function isExtendedError (constructor, err) { + return err instanceof constructor || (err && err.name === constructor.name) +} + function extend (a, b) { for (var key in b) { a[key] = b[key]; @@ -37,6 +41,10 @@ var View = { name: { type: String, default: 'default' + }, + nextLayer: { + type: Boolean, + default: false } }, render: function render (_, ref) { @@ -52,9 +60,16 @@ var View = { // so that components rendered by router-view can resolve named slots var h = parent.$createElement; var name = props.name; - var route = parent.$route; var cache = parent._routerViewCache || (parent._routerViewCache = {}); + var layer = parent._routerLayer + (props.nextLayer ? 1 : 0); + // render empty node if we don't have this high layers + if (parent._routerRoot._routes.length <= layer) { + cache[name] = null; + return h() + } + var route = parent._routerRoot._routes[layer]; + // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. var depth = 0; @@ -98,6 +113,7 @@ var View = { ) { matched.instances[name] = val; } + vm._routerLayer = layer; } // also register instance in prepatch hook @@ -392,200 +408,6 @@ function queryIncludes (current, target) { /* */ -// work around weird flow bug -var toTypes = [String, Object]; -var eventTypes = [String, Array]; - -var Link = { - name: 'RouterLink', - props: { - to: { - type: toTypes, - required: true - }, - tag: { - type: String, - default: 'a' - }, - exact: Boolean, - append: Boolean, - replace: Boolean, - activeClass: String, - exactActiveClass: String, - event: { - type: eventTypes, - default: 'click' - } - }, - render: function render (h) { - var this$1 = this; - - var router = this.$router; - var current = this.$route; - var ref = router.resolve(this.to, current, this.append); - var location = ref.location; - var route = ref.route; - var href = ref.href; - - var classes = {}; - var globalActiveClass = router.options.linkActiveClass; - var globalExactActiveClass = router.options.linkExactActiveClass; - // Support global empty active class - var activeClassFallback = globalActiveClass == null - ? 'router-link-active' - : globalActiveClass; - var exactActiveClassFallback = globalExactActiveClass == null - ? 'router-link-exact-active' - : globalExactActiveClass; - var activeClass = this.activeClass == null - ? activeClassFallback - : this.activeClass; - var exactActiveClass = this.exactActiveClass == null - ? exactActiveClassFallback - : this.exactActiveClass; - var compareTarget = location.path - ? createRoute(null, location, null, router) - : route; - - classes[exactActiveClass] = isSameRoute(current, compareTarget); - classes[activeClass] = this.exact - ? classes[exactActiveClass] - : isIncludedRoute(current, compareTarget); - - var handler = function (e) { - if (guardEvent(e)) { - if (this$1.replace) { - router.replace(location); - } else { - router.push(location); - } - } - }; - - var on = { click: guardEvent }; - if (Array.isArray(this.event)) { - this.event.forEach(function (e) { on[e] = handler; }); - } else { - on[this.event] = handler; - } - - var data = { - class: classes - }; - - if (this.tag === 'a') { - data.on = on; - data.attrs = { href: href }; - } else { - // find the first child and apply listener and href - var a = findAnchor(this.$slots.default); - if (a) { - // in case the is a static node - a.isStatic = false; - var aData = a.data = extend({}, a.data); - aData.on = on; - var aAttrs = a.data.attrs = extend({}, a.data.attrs); - aAttrs.href = href; - } else { - // doesn't have child, apply listener to self - data.on = on; - } - } - - return h(this.tag, data, this.$slots.default) - } -} - -function guardEvent (e) { - // don't redirect with control keys - if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return } - // don't redirect when preventDefault called - if (e.defaultPrevented) { return } - // don't redirect on right click - if (e.button !== undefined && e.button !== 0) { return } - // don't redirect if `target="_blank"` - if (e.currentTarget && e.currentTarget.getAttribute) { - var target = e.currentTarget.getAttribute('target'); - if (/\b_blank\b/i.test(target)) { return } - } - // this may be a Weex event which doesn't have this method - if (e.preventDefault) { - e.preventDefault(); - } - return true -} - -function findAnchor (children) { - if (children) { - var child; - for (var i = 0; i < children.length; i++) { - child = children[i]; - if (child.tag === 'a') { - return child - } - if (child.children && (child = findAnchor(child.children))) { - return child - } - } - } -} - -var _Vue; - -function install (Vue) { - if (install.installed && _Vue === Vue) { return } - install.installed = true; - - _Vue = Vue; - - var isDef = function (v) { return v !== undefined; }; - - var registerInstance = function (vm, callVal) { - var i = vm.$options._parentVnode; - if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { - i(vm, callVal); - } - }; - - Vue.mixin({ - beforeCreate: function beforeCreate () { - if (isDef(this.$options.router)) { - this._routerRoot = this; - this._router = this.$options.router; - this._router.init(this); - Vue.util.defineReactive(this, '_route', this._router.history.current); - } else { - this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; - } - registerInstance(this, this); - }, - destroyed: function destroyed () { - registerInstance(this); - } - }); - - Object.defineProperty(Vue.prototype, '$router', { - get: function get () { return this._routerRoot._router } - }); - - Object.defineProperty(Vue.prototype, '$route', { - get: function get () { return this._routerRoot._route } - }); - - Vue.component('RouterView', View); - Vue.component('RouterLink', Link); - - var strats = Vue.config.optionMergeStrategies; - // use the same hook merging strategy for route hooks - strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; -} - -/* */ - -var inBrowser = typeof window !== 'undefined'; - -/* */ - function resolvePath ( relative, base, @@ -1050,77 +872,368 @@ function tokensToRegExp (tokens, keys, options) { route += strict && endsWithDelimiter ? '' : '(?=' + delimiter + '|$)'; } - return attachKeys(new RegExp('^' + route, flags(options)), keys) -} + return attachKeys(new RegExp('^' + route, flags(options)), keys) +} + +/** + * Normalize the given path string, returning a regular expression. + * + * An empty array can be passed in for the keys, which will hold the + * placeholder key descriptions. For example, using `/user/:id`, `keys` will + * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. + * + * @param {(string|RegExp|Array)} path + * @param {(Array|Object)=} keys + * @param {Object=} options + * @return {!RegExp} + */ +function pathToRegexp (path, keys, options) { + if (!isarray(keys)) { + options = /** @type {!Object} */ (keys || options); + keys = []; + } + + options = options || {}; + + if (path instanceof RegExp) { + return regexpToRegexp(path, /** @type {!Array} */ (keys)) + } + + if (isarray(path)) { + return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) + } + + return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) +} +pathToRegexp_1.parse = parse_1; +pathToRegexp_1.compile = compile_1; +pathToRegexp_1.tokensToFunction = tokensToFunction_1; +pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + +/* */ + +// $flow-disable-line +var regexpCompileCache = Object.create(null); + +function fillParams ( + path, + params, + routeMsg +) { + params = params || {}; + try { + var filler = + regexpCompileCache[path] || + (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + + // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} + if (params.pathMatch) { params[0] = params.pathMatch; } + + return filler(params, { pretty: true }) + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + warn(false, ("missing param for " + routeMsg + ": " + (e.message))); + } + return '' + } finally { + // delete the 0 if it was added + delete params[0]; + } +} + +/* */ + +function normalizeLocation ( + raw, + current, + append, + router +) { + var next = typeof raw === 'string' ? { path: raw } : raw; + // named target + if (next._normalized) { + return next + } else if (next.name) { + return extend({}, raw) + } + + // relative params + if (!next.path && next.params && current) { + next = extend({}, next); + next._normalized = true; + var params = extend(extend({}, current.params), next.params); + if (current.name) { + next.name = current.name; + next.params = params; + } else if (current.matched.length) { + var rawPath = current.matched[current.matched.length - 1].path; + next.path = fillParams(rawPath, params, ("path " + (current.path))); + } else if (process.env.NODE_ENV !== 'production') { + warn(false, "relative params navigation requires a current route."); + } + return next + } + + var parsedPath = parsePath(next.path || ''); + var basePath = (current && current.path) || '/'; + var path = parsedPath.path + ? resolvePath(parsedPath.path, basePath, append || next.append) + : basePath; + + var query = resolveQuery( + parsedPath.query, + next.query, + router && router.options.parseQuery + ); + + var hash = next.hash || parsedPath.hash; + if (hash && hash.charAt(0) !== '#') { + hash = "#" + hash; + } + + return { + _normalized: true, + path: path, + query: query, + hash: hash + } +} + +/* */ + +// work around weird flow bug +var toTypes = [String, Object]; +var eventTypes = [String, Array]; + +var Link = { + name: 'RouterLink', + props: { + to: { + type: toTypes, + required: true + }, + tag: { + type: String, + default: 'a' + }, + exact: Boolean, + append: Boolean, + replace: Boolean, + addLayer: Boolean, + removeLayer: Boolean, + activeClass: String, + exactActiveClass: String, + event: { + type: eventTypes, + default: 'click' + } + }, + render: function render (h) { + var this$1 = this; + + var router = this.$router; + var current = this.$route; + var ref = router.resolve( + this.to, + current, + this.append + ); + var location = ref.location; + var route = ref.route; + var href = ref.href; + + var classes = {}; + var globalActiveClass = router.options.linkActiveClass; + var globalExactActiveClass = router.options.linkExactActiveClass; + // Support global empty active class + var activeClassFallback = + globalActiveClass == null ? 'router-link-active' : globalActiveClass; + var exactActiveClassFallback = + globalExactActiveClass == null + ? 'router-link-exact-active' + : globalExactActiveClass; + var activeClass = + this.activeClass == null ? activeClassFallback : this.activeClass; + var exactActiveClass = + this.exactActiveClass == null + ? exactActiveClassFallback + : this.exactActiveClass; + + var compareTarget = route.redirectedFrom + ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router) + : route; + + classes[exactActiveClass] = isSameRoute(current, compareTarget); + classes[activeClass] = this.exact + ? classes[exactActiveClass] + : isIncludedRoute(current, compareTarget); + + var handler = function (e) { + if (guardEvent(e)) { + if (this$1.replace) { + if (this$1.addLayer) { + router.replaceAddLayer(location); + } else if (this$1.removeLayer) { + router.replaceRemoveLayer(); + } else { + router.replaceLayer(this$1._routerLayer, location); + } + } else { + if (this$1.addLayer) { + router.pushAddLayer(location); + } else if (this$1.removeLayer) { + router.pushRemoveLayer(); + } else { + router.pushLayer(this$1._routerLayer, location); + } + } + } + }; + + var on = { click: guardEvent }; + if (Array.isArray(this.event)) { + this.event.forEach(function (e) { + on[e] = handler; + }); + } else { + on[this.event] = handler; + } + + var data = { + class: classes + }; + + if (this.tag === 'a') { + data.on = on; + data.attrs = { href: href }; + } else { + // find the first child and apply listener and href + var a = findAnchor(this.$slots.default); + if (a) { + // in case the is a static node + a.isStatic = false; + var aData = (a.data = extend({}, a.data)); + aData.on = on; + var aAttrs = (a.data.attrs = extend({}, a.data.attrs)); + aAttrs.href = href; + } else { + // doesn't have child, apply listener to self + data.on = on; + } + } + + return h(this.tag, data, this.$slots.default) + } +} + +function guardEvent (e) { + // don't redirect with control keys + if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return } + // don't redirect when preventDefault called + if (e.defaultPrevented) { return } + // don't redirect on right click + if (e.button !== undefined && e.button !== 0) { return } + // don't redirect if `target="_blank"` + if (e.currentTarget && e.currentTarget.getAttribute) { + var target = e.currentTarget.getAttribute('target'); + if (/\b_blank\b/i.test(target)) { return } + } + // this may be a Weex event which doesn't have this method + if (e.preventDefault) { + e.preventDefault(); + } + return true +} + +function findAnchor (children) { + if (children) { + var child; + for (var i = 0; i < children.length; i++) { + child = children[i]; + if (child.tag === 'a') { + return child + } + if (child.children && (child = findAnchor(child.children))) { + return child + } + } + } +} + +var _Vue; +var VueRouterLayer = function VueRouterLayer (router, layer) { + this.router = router; + this.layer = layer; +}; + +VueRouterLayer.prototype.push = function push (location, onComplete, onAbort) { + return this.router.pushLayer(this.layer, location, onComplete, onAbort) +}; + +VueRouterLayer.prototype.replace = function replace (location, onComplete, onAbort) { + return this.router.replaceLayer(this.layer, location, onComplete, onAbort) +}; -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - * - * @param {(string|RegExp|Array)} path - * @param {(Array|Object)=} keys - * @param {Object=} options - * @return {!RegExp} - */ -function pathToRegexp (path, keys, options) { - if (!isarray(keys)) { - options = /** @type {!Object} */ (keys || options); - keys = []; - } +function install (Vue) { + if (install.installed && _Vue === Vue) { return } + install.installed = true; - options = options || {}; + _Vue = Vue; - if (path instanceof RegExp) { - return regexpToRegexp(path, /** @type {!Array} */ (keys)) - } + var isDef = function (v) { return v !== undefined; }; - if (isarray(path)) { - return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) - } + var registerInstance = function (vm, callVal) { + var i = vm.$options._parentVnode; + if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { + i(vm, callVal); + } + }; - return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) -} -pathToRegexp_1.parse = parse_1; -pathToRegexp_1.compile = compile_1; -pathToRegexp_1.tokensToFunction = tokensToFunction_1; -pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + Vue.mixin({ + beforeCreate: function beforeCreate () { + if (isDef(this.$options.router)) { + this._routerRoot = this; + this._router = this.$options.router; + this._router.init(this); + this._routerLayer = 0; + Vue.util.defineReactive(this, '_routes', this._router.history.current); + } else { + this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; + this._routerLayer = this.$parent ? this.$parent._routerLayer : 0; + } + registerInstance(this, this); + }, + destroyed: function destroyed () { + registerInstance(this); + } + }); -/* */ + Object.defineProperty(Vue.prototype, '$router', { + get: function get () { return this._routerRoot._router } + }); -// $flow-disable-line -var regexpCompileCache = Object.create(null); + Object.defineProperty(Vue.prototype, '$routerLayer', { + get: function get () { return new VueRouterLayer(this._routerRoot._router, this._routerLayer) } + }); -function fillParams ( - path, - params, - routeMsg -) { - params = params || {}; - try { - var filler = - regexpCompileCache[path] || - (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + Object.defineProperty(Vue.prototype, '$route', { + get: function get () { return this._routerRoot._routes[this._routerLayer] } + }); - // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} - if (params.pathMatch) { params[0] = params.pathMatch; } + Vue.component('RouterView', View); + Vue.component('RouterLink', Link); - return filler(params, { pretty: true }) - } catch (e) { - if (process.env.NODE_ENV !== 'production') { - warn(false, ("missing param for " + routeMsg + ": " + (e.message))); - } - return '' - } finally { - // delete the 0 if it was added - delete params[0]; - } + var strats = Vue.config.optionMergeStrategies; + // use the same hook merging strategy for route hooks + strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; } /* */ +var inBrowser = typeof window !== 'undefined'; + +/* */ + function createRouteMap ( routes, oldPathList, @@ -1286,64 +1399,6 @@ function normalizePath (path, parent, strict) { /* */ -function normalizeLocation ( - raw, - current, - append, - router -) { - var next = typeof raw === 'string' ? { path: raw } : raw; - // named target - if (next._normalized) { - return next - } else if (next.name) { - return extend({}, raw) - } - - // relative params - if (!next.path && next.params && current) { - next = extend({}, next); - next._normalized = true; - var params = extend(extend({}, current.params), next.params); - if (current.name) { - next.name = current.name; - next.params = params; - } else if (current.matched.length) { - var rawPath = current.matched[current.matched.length - 1].path; - next.path = fillParams(rawPath, params, ("path " + (current.path))); - } else if (process.env.NODE_ENV !== 'production') { - warn(false, "relative params navigation requires a current route."); - } - return next - } - - var parsedPath = parsePath(next.path || ''); - var basePath = (current && current.path) || '/'; - var path = parsedPath.path - ? resolvePath(parsedPath.path, basePath, append || next.append) - : basePath; - - var query = resolveQuery( - parsedPath.query, - next.query, - router && router.options.parseQuery - ); - - var hash = next.hash || parsedPath.hash; - if (hash && hash.charAt(0) !== '#') { - hash = "#" + hash; - } - - return { - _normalized: true, - path: path, - query: query, - hash: hash - } -} - -/* */ - function createMatcher ( @@ -1581,20 +1636,27 @@ function handleScroll ( // wait until re-render finishes before scrolling router.app.$nextTick(function () { var position = getScrollPosition(); - var shouldScroll = behavior.call(router, to, from, isPop ? position : null); + var shouldScroll = behavior.call( + router, + to, + from, + isPop ? position : null + ); if (!shouldScroll) { return } if (typeof shouldScroll.then === 'function') { - shouldScroll.then(function (shouldScroll) { - scrollToPosition((shouldScroll), position); - }).catch(function (err) { - if (process.env.NODE_ENV !== 'production') { - assert(false, err.toString()); - } - }); + shouldScroll + .then(function (shouldScroll) { + scrollToPosition((shouldScroll), position); + }) + .catch(function (err) { + if (process.env.NODE_ENV !== 'production') { + assert(false, err.toString()); + } + }); } else { scrollToPosition(shouldScroll, position); } @@ -1650,12 +1712,22 @@ function isNumber (v) { return typeof v === 'number' } +var hashStartsWithNumberRE = /^#\d/; + function scrollToPosition (shouldScroll, position) { var isObject = typeof shouldScroll === 'object'; if (isObject && typeof shouldScroll.selector === 'string') { - var el = document.querySelector(shouldScroll.selector); + // getElementById would still fail if the selector contains a more complicated query like #main[data-attr] + // but at the same time, it doesn't make much sense to select an element with an id and an extra selector + var el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line + ? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line + : document.querySelector(shouldScroll.selector); + if (el) { - var offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {}; + var offset = + shouldScroll.offset && typeof shouldScroll.offset === 'object' + ? shouldScroll.offset + : {}; offset = normalizeOffset(offset); position = getElementPosition(el, offset); } else if (isValidPosition(shouldScroll)) { @@ -1706,17 +1778,17 @@ function setStateKey (key) { _key = key; } -function pushState (url, replace) { +function pushState (url, state, replace) { saveScrollPosition(); // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls var history = window.history; try { if (replace) { - history.replaceState({ key: _key }, '', url); + history.replaceState({ key: _key, state: state }, '', url); } else { _key = genKey(); - history.pushState({ key: _key }, '', url); + history.pushState({ key: _key, state: state }, '', url); } } catch (e) { window.location[replace ? 'replace' : 'assign'](url); @@ -1855,13 +1927,26 @@ function once (fn) { } } +var NavigationDuplicated = /*@__PURE__*/(function (Error) { + function NavigationDuplicated () { + Error.call(this, 'Navigating to current location is not allowed'); + this.name = 'NavigationDuplicated'; + } + + if ( Error ) NavigationDuplicated.__proto__ = Error; + NavigationDuplicated.prototype = Object.create( Error && Error.prototype ); + NavigationDuplicated.prototype.constructor = NavigationDuplicated; + + return NavigationDuplicated; +}(Error)); + /* */ var History = function History (router, base) { this.router = router; this.base = normalizeBase(base); // start with a route object that stands for "nowhere" - this.current = START; + this.current = [START]; this.pending = null; this.ready = false; this.readyCbs = []; @@ -1888,39 +1973,64 @@ History.prototype.onError = function onError (errorCb) { this.errorCbs.push(errorCb); }; -History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) { - var this$1 = this; +History.prototype.getClosestCurrent = function getClosestCurrent (n) { + if (n >= this.current.length) { + return this.current[this.current.length - 1] + } + return this.current[n] +}; - var route = this.router.match(location, this.current); - this.confirmTransition(route, function () { - this$1.updateRoute(route); - onComplete && onComplete(route); - this$1.ensureURL(); +History.prototype.transitionTo = function transitionTo ( + locations, + onComplete, + onAbort +) { + var this$1 = this; - // fire ready cbs once - if (!this$1.ready) { - this$1.ready = true; - this$1.readyCbs.forEach(function (cb) { cb(route); }); - } - }, function (err) { - if (onAbort) { - onAbort(err); - } - if (err && !this$1.ready) { - this$1.ready = true; - this$1.readyErrorCbs.forEach(function (cb) { cb(err); }); + var routes = locations.map(function (location, i) { return this$1.router.match(location, this$1.getClosestCurrent(i)); }); + this.confirmTransition( + routes, + function (equalLayers) { + this$1.updateCurrent(routes, equalLayers); + onComplete && onComplete(routes); + this$1.ensureURL(); + + // fire ready cbs once + if (!this$1.ready) { + this$1.ready = true; + this$1.readyCbs.forEach(function (cb) { + cb(routes); + }); + } + }, + function (err) { + if (onAbort) { + onAbort(err); + } + if (err && !this$1.ready) { + this$1.ready = true; + this$1.readyErrorCbs.forEach(function (cb) { + cb(err); + }); + } } - }); + ); }; -History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { +History.prototype.confirmTransition = function confirmTransition (routes, onComplete, onAbort) { var this$1 = this; var current = this.current; var abort = function (err) { - if (isError(err)) { + // after merging https://github.com/vuejs/vue-router/pull/2771 we + // When the user navigates through history through back/forward buttons + // we do not want to throw the error. We only throw it if directly calling + // push/replace. That's why it's not included in isError + if (!isExtendedError(NavigationDuplicated, err) && isError(err)) { if (this$1.errorCbs.length) { - this$1.errorCbs.forEach(function (cb) { cb(err); }); + this$1.errorCbs.forEach(function (cb) { + cb(err); + }); } else { warn(false, 'uncaught error during route navigation:'); console.error(err); @@ -1928,16 +2038,28 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl } onAbort && onAbort(err); }; - if ( - isSameRoute(route, current) && - // in the case the route map has been dynamically appended to - route.matched.length === current.matched.length - ) { + + var equalLayers = 0; + for (equalLayers = 0; equalLayers < routes.length && equalLayers < current.length; equalLayers++) { + if ( + !isSameRoute(routes[equalLayers], current[equalLayers]) || + // in the case the route map has been dynamically appended to + routes[equalLayers].matched.length !== current[equalLayers].matched.length + ) { + break + } + } + + if (equalLayers === routes.length && equalLayers === current.length) { this.ensureURL(); - return abort() + return abort(new NavigationDuplicated(routes)) } - var ref = resolveQueue(this.current.matched, route.matched); + var ref = resolveQueues( + current, + routes, + equalLayers + ); var updated = ref.updated; var deactivated = ref.deactivated; var activated = ref.activated; @@ -1955,23 +2077,21 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl resolveAsyncComponents(activated) ); - this.pending = route; + this.pending = routes; var iterator = function (hook, next) { - if (this$1.pending !== route) { + if (this$1.pending !== routes) { return abort() } try { - hook(route, current, function (to) { + hook(routes, current, function (to) { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this$1.ensureURL(true); abort(to); } else if ( typeof to === 'string' || - (typeof to === 'object' && ( - typeof to.path === 'string' || - typeof to.name === 'string' - )) + (typeof to === 'object' && + (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({ path: '/' }) -> redirect abort(); @@ -1992,32 +2112,34 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl runQueue(queue, iterator, function () { var postEnterCbs = []; - var isValid = function () { return this$1.current === route; }; + var isValid = function () { return this$1.current === routes; }; // wait until async components are resolved before // extracting in-component enter guards var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); var queue = enterGuards.concat(this$1.router.resolveHooks); runQueue(queue, iterator, function () { - if (this$1.pending !== route) { + if (this$1.pending !== routes) { return abort() } this$1.pending = null; - onComplete(route); + onComplete(equalLayers); if (this$1.router.app) { this$1.router.app.$nextTick(function () { - postEnterCbs.forEach(function (cb) { cb(); }); + postEnterCbs.forEach(function (cb) { + cb(); + }); }); } }); }); }; -History.prototype.updateRoute = function updateRoute (route) { +History.prototype.updateCurrent = function updateCurrent (routes, equalLayers) { var prev = this.current; - this.current = route; - this.cb && this.cb(route); + this.current = routes; + this.cb && this.cb(routes, equalLayers); this.router.afterHooks.forEach(function (hook) { - hook && hook(route, prev); + hook && hook(routes, prev); }); }; @@ -2041,6 +2163,35 @@ function normalizeBase (base) { return base.replace(/\/$/, '') } +function resolveQueues ( + current, + next, + equalLayers +) { + var ref, ref$1, ref$2, ref$3, ref$4; + + var res = { + updated: [], + activated: [], + deactivated: [] + }; + + var min = Math.min(current.length, next.length); + for (var i = equalLayers; i < min; i++) { + var r = resolveQueue(current[i].matched, next[i].matched) + (ref = res.updated).push.apply(ref, r.updated) + (ref$1 = res.activated).push.apply(ref$1, r.activated) + (ref$2 = res.deactivated).push.apply(ref$2, r.deactivated); + } + for (var i$1 = min; i$1 < current.length; i$1++) { + (ref$3 = res.deactivated).push.apply(ref$3, current[i$1].matched); + } + for (var i$2 = min; i$2 < next.length; i$2++) { + (ref$4 = res.activated).push.apply(ref$4, next[i$2].matched); + } + return res +} + function resolveQueue ( current, next @@ -2108,9 +2259,13 @@ function extractEnterGuards ( cbs, isValid ) { - return extractGuards(activated, 'beforeRouteEnter', function (guard, _, match, key) { - return bindEnterGuard(guard, match, key, cbs, isValid) - }) + return extractGuards( + activated, + 'beforeRouteEnter', + function (guard, _, match, key) { + return bindEnterGuard(guard, match, key, cbs, isValid) + } + ) } function bindEnterGuard ( @@ -2181,7 +2336,11 @@ var HTML5History = /*@__PURE__*/(function (History$$1) { return } - this$1.transitionTo(location, function (route) { + var locations = [location]; + if (window.history.state.state) { + locations = window.history.state.state; + } + this$1.transitionTo(locations, function (route) { if (supportsScroll) { handleScroll(router, route, current, true); } @@ -2197,34 +2356,48 @@ var HTML5History = /*@__PURE__*/(function (History$$1) { window.history.go(n); }; - HTML5History.prototype.push = function push (location, onComplete, onAbort) { + HTML5History.prototype.navigateAllLayers = function navigateAllLayers (locations, push, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; - this.transitionTo(location, function (route) { - pushState(cleanPath(this$1.base + route.fullPath)); + this.transitionTo(locations, function (routes) { + this$1.ensureURL(push); + var route = this$1.current[this$1.current.length - 1]; handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }; - HTML5History.prototype.replace = function replace (location, onComplete, onAbort) { - var this$1 = this; + HTML5History.prototype.navigateLastLayer = function navigateLastLayer (location, push, onComplete, onAbort) { + var locations = this.current.slice(0, -1).map(function (r) { return r.fullPath; }).concat( [location] + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; - var ref = this; - var fromRoute = ref.current; - this.transitionTo(location, function (route) { - replaceState(cleanPath(this$1.base + route.fullPath)); - handleScroll(this$1.router, route, fromRoute, false); - onComplete && onComplete(route); - }, onAbort); + HTML5History.prototype.navigateLayer = function navigateLayer (layer, location, push, onComplete, onAbort) { + var locations = this.current.slice(0, layer).map(function (r) { return r.fullPath; }).concat( [location], + this.current.slice(layer + 1).map(function (r) { return r.fullPath; }) + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; + + HTML5History.prototype.navigateAddLayer = function navigateAddLayer (location, push, onComplete, onAbort) { + var locations = this.current.map(function (r) { return r.fullPath; }).concat( [location] + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; + + HTML5History.prototype.navigateRemoveLayer = function navigateRemoveLayer (location, push, onComplete, onAbort) { + var locations = this.current.slice(0, -1).map(function (r) { return r.fullPath; }); + this.navigateAllLayers(locations, push, onComplete, onAbort); }; HTML5History.prototype.ensureURL = function ensureURL (push) { - if (getLocation(this.base) !== this.current.fullPath) { - var current = cleanPath(this.base + this.current.fullPath); - push ? pushState(current) : replaceState(current); + var route = this.current[this.current.length - 1]; + if (getLocation(this.base) !== route.fullPath) { + var path = cleanPath(this.base + route.fullPath); + pushState(path, this.current.map(function (r) { return r.fullPath; }), !push); } }; @@ -2412,20 +2585,28 @@ var AbstractHistory = /*@__PURE__*/(function (History$$1) { AbstractHistory.prototype.push = function push (location, onComplete, onAbort) { var this$1 = this; - this.transitionTo(location, function (route) { - this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route); - this$1.index++; - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + function (route) { + this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route); + this$1.index++; + onComplete && onComplete(route); + }, + onAbort + ); }; AbstractHistory.prototype.replace = function replace (location, onComplete, onAbort) { var this$1 = this; - this.transitionTo(location, function (route) { - this$1.stack = this$1.stack.slice(0, this$1.index).concat(route); - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + function (route) { + this$1.stack = this$1.stack.slice(0, this$1.index).concat(route); + onComplete && onComplete(route); + }, + onAbort + ); }; AbstractHistory.prototype.go = function go (n) { @@ -2436,10 +2617,18 @@ var AbstractHistory = /*@__PURE__*/(function (History$$1) { return } var route = this.stack[targetIndex]; - this.confirmTransition(route, function () { - this$1.index = targetIndex; - this$1.updateRoute(route); - }); + this.confirmTransition( + route, + function () { + this$1.index = targetIndex; + this$1.updateRoute(route); + }, + function (err) { + if (isExtendedError(NavigationDuplicated, err)) { + this$1.index = targetIndex; + } + } + ); }; AbstractHistory.prototype.getCurrentLocation = function getCurrentLocation () { @@ -2543,7 +2732,7 @@ VueRouter.prototype.init = function init (app /* Vue component instance */) { var history = this.history; if (history instanceof HTML5History) { - history.transitionTo(history.getCurrentLocation()); + history.transitionTo([history.getCurrentLocation()]); } else if (history instanceof HashHistory) { var setupHashListener = function () { history.setupListeners(); @@ -2555,9 +2744,21 @@ VueRouter.prototype.init = function init (app /* Vue component instance */) { ); } - history.listen(function (route) { + history.listen(function (routes, equalLayers) { this$1.apps.forEach(function (app) { - app._route = route; + // Mutate only the changed routes, so we don't trigger unnecessary updates. + // Changed elements + for (var i = equalLayers; i < app._routes.length && i < routes.length; i++) { + app.$set(app._routes, i, routes[i]); + } + // Added elements + for (var i$1 = app._routes.length; i$1 < routes.length; i$1++) { + app._routes.push(routes[i$1]); + } + // Removed + while (app._routes.length > routes.length) { + app._routes.pop(); + } }); }); }; @@ -2583,11 +2784,43 @@ VueRouter.prototype.onError = function onError (errorCb) { }; VueRouter.prototype.push = function push (location, onComplete, onAbort) { - this.history.push(location, onComplete, onAbort); + this.history.navigateLastLayer(location, true, onComplete, onAbort); }; VueRouter.prototype.replace = function replace (location, onComplete, onAbort) { - this.history.replace(location, onComplete, onAbort); + this.history.navigateLastLayer(location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushAddLayer = function pushAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceAddLayer = function replaceAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushRemoveLayer = function pushRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceRemoveLayer = function replaceRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(false, onComplete, onAbort); +}; + +VueRouter.prototype.pushLayer = function pushLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceLayer = function replaceLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushAllLayers = function pushAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceAllLayers = function replaceAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, false, onComplete, onAbort); }; VueRouter.prototype.go = function go (n) { diff --git a/dist/vue-router.esm.browser.js b/dist/vue-router.esm.browser.js index 2602ba302..0b50c04e4 100644 --- a/dist/vue-router.esm.browser.js +++ b/dist/vue-router.esm.browser.js @@ -21,6 +21,10 @@ function isError (err) { return Object.prototype.toString.call(err).indexOf('Error') > -1 } +function isExtendedError (constructor, err) { + return err instanceof constructor || (err && err.name === constructor.name) +} + function extend (a, b) { for (const key in b) { a[key] = b[key]; @@ -35,6 +39,10 @@ var View = { name: { type: String, default: 'default' + }, + nextLayer: { + type: Boolean, + default: false } }, render (_, { props, children, parent, data }) { @@ -45,9 +53,16 @@ var View = { // so that components rendered by router-view can resolve named slots const h = parent.$createElement; const name = props.name; - const route = parent.$route; const cache = parent._routerViewCache || (parent._routerViewCache = {}); + const layer = parent._routerLayer + (props.nextLayer ? 1 : 0); + // render empty node if we don't have this high layers + if (parent._routerRoot._routes.length <= layer) { + cache[name] = null; + return h() + } + const route = parent._routerRoot._routes[layer]; + // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. let depth = 0; @@ -91,6 +106,7 @@ var View = { ) { matched.instances[name] = val; } + vm._routerLayer = layer; } // also register instance in prepatch hook @@ -376,195 +392,6 @@ function queryIncludes (current, target) { /* */ -// work around weird flow bug -const toTypes = [String, Object]; -const eventTypes = [String, Array]; - -var Link = { - name: 'RouterLink', - props: { - to: { - type: toTypes, - required: true - }, - tag: { - type: String, - default: 'a' - }, - exact: Boolean, - append: Boolean, - replace: Boolean, - activeClass: String, - exactActiveClass: String, - event: { - type: eventTypes, - default: 'click' - } - }, - render (h) { - const router = this.$router; - const current = this.$route; - const { location, route, href } = router.resolve(this.to, current, this.append); - - const classes = {}; - const globalActiveClass = router.options.linkActiveClass; - const globalExactActiveClass = router.options.linkExactActiveClass; - // Support global empty active class - const activeClassFallback = globalActiveClass == null - ? 'router-link-active' - : globalActiveClass; - const exactActiveClassFallback = globalExactActiveClass == null - ? 'router-link-exact-active' - : globalExactActiveClass; - const activeClass = this.activeClass == null - ? activeClassFallback - : this.activeClass; - const exactActiveClass = this.exactActiveClass == null - ? exactActiveClassFallback - : this.exactActiveClass; - const compareTarget = location.path - ? createRoute(null, location, null, router) - : route; - - classes[exactActiveClass] = isSameRoute(current, compareTarget); - classes[activeClass] = this.exact - ? classes[exactActiveClass] - : isIncludedRoute(current, compareTarget); - - const handler = e => { - if (guardEvent(e)) { - if (this.replace) { - router.replace(location); - } else { - router.push(location); - } - } - }; - - const on = { click: guardEvent }; - if (Array.isArray(this.event)) { - this.event.forEach(e => { on[e] = handler; }); - } else { - on[this.event] = handler; - } - - const data = { - class: classes - }; - - if (this.tag === 'a') { - data.on = on; - data.attrs = { href }; - } else { - // find the first child and apply listener and href - const a = findAnchor(this.$slots.default); - if (a) { - // in case the is a static node - a.isStatic = false; - const aData = a.data = extend({}, a.data); - aData.on = on; - const aAttrs = a.data.attrs = extend({}, a.data.attrs); - aAttrs.href = href; - } else { - // doesn't have child, apply listener to self - data.on = on; - } - } - - return h(this.tag, data, this.$slots.default) - } -} - -function guardEvent (e) { - // don't redirect with control keys - if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return - // don't redirect when preventDefault called - if (e.defaultPrevented) return - // don't redirect on right click - if (e.button !== undefined && e.button !== 0) return - // don't redirect if `target="_blank"` - if (e.currentTarget && e.currentTarget.getAttribute) { - const target = e.currentTarget.getAttribute('target'); - if (/\b_blank\b/i.test(target)) return - } - // this may be a Weex event which doesn't have this method - if (e.preventDefault) { - e.preventDefault(); - } - return true -} - -function findAnchor (children) { - if (children) { - let child; - for (let i = 0; i < children.length; i++) { - child = children[i]; - if (child.tag === 'a') { - return child - } - if (child.children && (child = findAnchor(child.children))) { - return child - } - } - } -} - -let _Vue; - -function install (Vue) { - if (install.installed && _Vue === Vue) return - install.installed = true; - - _Vue = Vue; - - const isDef = v => v !== undefined; - - const registerInstance = (vm, callVal) => { - let i = vm.$options._parentVnode; - if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { - i(vm, callVal); - } - }; - - Vue.mixin({ - beforeCreate () { - if (isDef(this.$options.router)) { - this._routerRoot = this; - this._router = this.$options.router; - this._router.init(this); - Vue.util.defineReactive(this, '_route', this._router.history.current); - } else { - this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; - } - registerInstance(this, this); - }, - destroyed () { - registerInstance(this); - } - }); - - Object.defineProperty(Vue.prototype, '$router', { - get () { return this._routerRoot._router } - }); - - Object.defineProperty(Vue.prototype, '$route', { - get () { return this._routerRoot._route } - }); - - Vue.component('RouterView', View); - Vue.component('RouterLink', Link); - - const strats = Vue.config.optionMergeStrategies; - // use the same hook merging strategy for route hooks - strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; -} - -/* */ - -const inBrowser = typeof window !== 'undefined'; - -/* */ - function resolvePath ( relative, base, @@ -1050,56 +877,344 @@ function pathToRegexp (path, keys, options) { keys = []; } - options = options || {}; - - if (path instanceof RegExp) { - return regexpToRegexp(path, /** @type {!Array} */ (keys)) + options = options || {}; + + if (path instanceof RegExp) { + return regexpToRegexp(path, /** @type {!Array} */ (keys)) + } + + if (isarray(path)) { + return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) + } + + return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) +} +pathToRegexp_1.parse = parse_1; +pathToRegexp_1.compile = compile_1; +pathToRegexp_1.tokensToFunction = tokensToFunction_1; +pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + +/* */ + +// $flow-disable-line +const regexpCompileCache = Object.create(null); + +function fillParams ( + path, + params, + routeMsg +) { + params = params || {}; + try { + const filler = + regexpCompileCache[path] || + (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + + // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} + if (params.pathMatch) params[0] = params.pathMatch; + + return filler(params, { pretty: true }) + } catch (e) { + { + warn(false, `missing param for ${routeMsg}: ${e.message}`); + } + return '' + } finally { + // delete the 0 if it was added + delete params[0]; + } +} + +/* */ + +function normalizeLocation ( + raw, + current, + append, + router +) { + let next = typeof raw === 'string' ? { path: raw } : raw; + // named target + if (next._normalized) { + return next + } else if (next.name) { + return extend({}, raw) + } + + // relative params + if (!next.path && next.params && current) { + next = extend({}, next); + next._normalized = true; + const params = extend(extend({}, current.params), next.params); + if (current.name) { + next.name = current.name; + next.params = params; + } else if (current.matched.length) { + const rawPath = current.matched[current.matched.length - 1].path; + next.path = fillParams(rawPath, params, `path ${current.path}`); + } else { + warn(false, `relative params navigation requires a current route.`); + } + return next + } + + const parsedPath = parsePath(next.path || ''); + const basePath = (current && current.path) || '/'; + const path = parsedPath.path + ? resolvePath(parsedPath.path, basePath, append || next.append) + : basePath; + + const query = resolveQuery( + parsedPath.query, + next.query, + router && router.options.parseQuery + ); + + let hash = next.hash || parsedPath.hash; + if (hash && hash.charAt(0) !== '#') { + hash = `#${hash}`; + } + + return { + _normalized: true, + path, + query, + hash + } +} + +/* */ + +// work around weird flow bug +const toTypes = [String, Object]; +const eventTypes = [String, Array]; + +var Link = { + name: 'RouterLink', + props: { + to: { + type: toTypes, + required: true + }, + tag: { + type: String, + default: 'a' + }, + exact: Boolean, + append: Boolean, + replace: Boolean, + addLayer: Boolean, + removeLayer: Boolean, + activeClass: String, + exactActiveClass: String, + event: { + type: eventTypes, + default: 'click' + } + }, + render (h) { + const router = this.$router; + const current = this.$route; + const { location, route, href } = router.resolve( + this.to, + current, + this.append + ); + + const classes = {}; + const globalActiveClass = router.options.linkActiveClass; + const globalExactActiveClass = router.options.linkExactActiveClass; + // Support global empty active class + const activeClassFallback = + globalActiveClass == null ? 'router-link-active' : globalActiveClass; + const exactActiveClassFallback = + globalExactActiveClass == null + ? 'router-link-exact-active' + : globalExactActiveClass; + const activeClass = + this.activeClass == null ? activeClassFallback : this.activeClass; + const exactActiveClass = + this.exactActiveClass == null + ? exactActiveClassFallback + : this.exactActiveClass; + + const compareTarget = route.redirectedFrom + ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router) + : route; + + classes[exactActiveClass] = isSameRoute(current, compareTarget); + classes[activeClass] = this.exact + ? classes[exactActiveClass] + : isIncludedRoute(current, compareTarget); + + const handler = e => { + if (guardEvent(e)) { + if (this.replace) { + if (this.addLayer) { + router.replaceAddLayer(location); + } else if (this.removeLayer) { + router.replaceRemoveLayer(); + } else { + router.replaceLayer(this._routerLayer, location); + } + } else { + if (this.addLayer) { + router.pushAddLayer(location); + } else if (this.removeLayer) { + router.pushRemoveLayer(); + } else { + router.pushLayer(this._routerLayer, location); + } + } + } + }; + + const on = { click: guardEvent }; + if (Array.isArray(this.event)) { + this.event.forEach(e => { + on[e] = handler; + }); + } else { + on[this.event] = handler; + } + + const data = { + class: classes + }; + + if (this.tag === 'a') { + data.on = on; + data.attrs = { href }; + } else { + // find the first child and apply listener and href + const a = findAnchor(this.$slots.default); + if (a) { + // in case the is a static node + a.isStatic = false; + const aData = (a.data = extend({}, a.data)); + aData.on = on; + const aAttrs = (a.data.attrs = extend({}, a.data.attrs)); + aAttrs.href = href; + } else { + // doesn't have child, apply listener to self + data.on = on; + } + } + + return h(this.tag, data, this.$slots.default) + } +} + +function guardEvent (e) { + // don't redirect with control keys + if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return + // don't redirect when preventDefault called + if (e.defaultPrevented) return + // don't redirect on right click + if (e.button !== undefined && e.button !== 0) return + // don't redirect if `target="_blank"` + if (e.currentTarget && e.currentTarget.getAttribute) { + const target = e.currentTarget.getAttribute('target'); + if (/\b_blank\b/i.test(target)) return + } + // this may be a Weex event which doesn't have this method + if (e.preventDefault) { + e.preventDefault(); + } + return true +} + +function findAnchor (children) { + if (children) { + let child; + for (let i = 0; i < children.length; i++) { + child = children[i]; + if (child.tag === 'a') { + return child + } + if (child.children && (child = findAnchor(child.children))) { + return child + } + } + } +} + +let _Vue; +class VueRouterLayer { + constructor (router, layer) { + this.router = router; + this.layer = layer; } - if (isarray(path)) { - return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) + push (location, onComplete, onAbort) { + return this.router.pushLayer(this.layer, location, onComplete, onAbort) } - return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) + replace (location, onComplete, onAbort) { + return this.router.replaceLayer(this.layer, location, onComplete, onAbort) + } } -pathToRegexp_1.parse = parse_1; -pathToRegexp_1.compile = compile_1; -pathToRegexp_1.tokensToFunction = tokensToFunction_1; -pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; -/* */ +function install (Vue) { + if (install.installed && _Vue === Vue) return + install.installed = true; -// $flow-disable-line -const regexpCompileCache = Object.create(null); + _Vue = Vue; -function fillParams ( - path, - params, - routeMsg -) { - params = params || {}; - try { - const filler = - regexpCompileCache[path] || - (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + const isDef = v => v !== undefined; - // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} - if (params.pathMatch) params[0] = params.pathMatch; + const registerInstance = (vm, callVal) => { + let i = vm.$options._parentVnode; + if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { + i(vm, callVal); + } + }; - return filler(params, { pretty: true }) - } catch (e) { - { - warn(false, `missing param for ${routeMsg}: ${e.message}`); + Vue.mixin({ + beforeCreate () { + if (isDef(this.$options.router)) { + this._routerRoot = this; + this._router = this.$options.router; + this._router.init(this); + this._routerLayer = 0; + Vue.util.defineReactive(this, '_routes', this._router.history.current); + } else { + this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; + this._routerLayer = this.$parent ? this.$parent._routerLayer : 0; + } + registerInstance(this, this); + }, + destroyed () { + registerInstance(this); } - return '' - } finally { - // delete the 0 if it was added - delete params[0]; - } + }); + + Object.defineProperty(Vue.prototype, '$router', { + get () { return this._routerRoot._router } + }); + + Object.defineProperty(Vue.prototype, '$routerLayer', { + get () { return new VueRouterLayer(this._routerRoot._router, this._routerLayer) } + }); + + Object.defineProperty(Vue.prototype, '$route', { + get () { return this._routerRoot._routes[this._routerLayer] } + }); + + Vue.component('RouterView', View); + Vue.component('RouterLink', Link); + + const strats = Vue.config.optionMergeStrategies; + // use the same hook merging strategy for route hooks + strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; } /* */ +const inBrowser = typeof window !== 'undefined'; + +/* */ + function createRouteMap ( routes, oldPathList, @@ -1264,64 +1379,6 @@ function normalizePath (path, parent, strict) { /* */ -function normalizeLocation ( - raw, - current, - append, - router -) { - let next = typeof raw === 'string' ? { path: raw } : raw; - // named target - if (next._normalized) { - return next - } else if (next.name) { - return extend({}, raw) - } - - // relative params - if (!next.path && next.params && current) { - next = extend({}, next); - next._normalized = true; - const params = extend(extend({}, current.params), next.params); - if (current.name) { - next.name = current.name; - next.params = params; - } else if (current.matched.length) { - const rawPath = current.matched[current.matched.length - 1].path; - next.path = fillParams(rawPath, params, `path ${current.path}`); - } else { - warn(false, `relative params navigation requires a current route.`); - } - return next - } - - const parsedPath = parsePath(next.path || ''); - const basePath = (current && current.path) || '/'; - const path = parsedPath.path - ? resolvePath(parsedPath.path, basePath, append || next.append) - : basePath; - - const query = resolveQuery( - parsedPath.query, - next.query, - router && router.options.parseQuery - ); - - let hash = next.hash || parsedPath.hash; - if (hash && hash.charAt(0) !== '#') { - hash = `#${hash}`; - } - - return { - _normalized: true, - path, - query, - hash - } -} - -/* */ - function createMatcher ( @@ -1553,20 +1610,27 @@ function handleScroll ( // wait until re-render finishes before scrolling router.app.$nextTick(() => { const position = getScrollPosition(); - const shouldScroll = behavior.call(router, to, from, isPop ? position : null); + const shouldScroll = behavior.call( + router, + to, + from, + isPop ? position : null + ); if (!shouldScroll) { return } if (typeof shouldScroll.then === 'function') { - shouldScroll.then(shouldScroll => { - scrollToPosition((shouldScroll), position); - }).catch(err => { - { - assert(false, err.toString()); - } - }); + shouldScroll + .then(shouldScroll => { + scrollToPosition((shouldScroll), position); + }) + .catch(err => { + { + assert(false, err.toString()); + } + }); } else { scrollToPosition(shouldScroll, position); } @@ -1622,12 +1686,22 @@ function isNumber (v) { return typeof v === 'number' } +const hashStartsWithNumberRE = /^#\d/; + function scrollToPosition (shouldScroll, position) { const isObject = typeof shouldScroll === 'object'; if (isObject && typeof shouldScroll.selector === 'string') { - const el = document.querySelector(shouldScroll.selector); + // getElementById would still fail if the selector contains a more complicated query like #main[data-attr] + // but at the same time, it doesn't make much sense to select an element with an id and an extra selector + const el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line + ? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line + : document.querySelector(shouldScroll.selector); + if (el) { - let offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {}; + let offset = + shouldScroll.offset && typeof shouldScroll.offset === 'object' + ? shouldScroll.offset + : {}; offset = normalizeOffset(offset); position = getElementPosition(el, offset); } else if (isValidPosition(shouldScroll)) { @@ -1678,17 +1752,17 @@ function setStateKey (key) { _key = key; } -function pushState (url, replace) { +function pushState (url, state, replace) { saveScrollPosition(); // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history; try { if (replace) { - history.replaceState({ key: _key }, '', url); + history.replaceState({ key: _key, state }, '', url); } else { _key = genKey(); - history.pushState({ key: _key }, '', url); + history.pushState({ key: _key, state }, '', url); } } catch (e) { window.location[replace ? 'replace' : 'assign'](url); @@ -1824,6 +1898,13 @@ function once (fn) { } } +class NavigationDuplicated extends Error { + constructor () { + super('Navigating to current location is not allowed'); + this.name = 'NavigationDuplicated'; + } +} + /* */ class History { @@ -1843,12 +1924,15 @@ class History { + + + constructor (router, base) { this.router = router; this.base = normalizeBase(base); // start with a route object that stands for "nowhere" - this.current = START; + this.current = [START]; this.pending = null; this.ready = false; this.readyCbs = []; @@ -1875,35 +1959,60 @@ class History { this.errorCbs.push(errorCb); } - transitionTo (location, onComplete, onAbort) { - const route = this.router.match(location, this.current); - this.confirmTransition(route, () => { - this.updateRoute(route); - onComplete && onComplete(route); - this.ensureURL(); + getClosestCurrent (n) { + if (n >= this.current.length) { + return this.current[this.current.length - 1] + } + return this.current[n] + } - // fire ready cbs once - if (!this.ready) { - this.ready = true; - this.readyCbs.forEach(cb => { cb(route); }); - } - }, err => { - if (onAbort) { - onAbort(err); - } - if (err && !this.ready) { - this.ready = true; - this.readyErrorCbs.forEach(cb => { cb(err); }); + transitionTo ( + locations, + onComplete, + onAbort + ) { + const routes = locations.map((location, i) => this.router.match(location, this.getClosestCurrent(i))); + this.confirmTransition( + routes, + (equalLayers) => { + this.updateCurrent(routes, equalLayers); + onComplete && onComplete(routes); + this.ensureURL(); + + // fire ready cbs once + if (!this.ready) { + this.ready = true; + this.readyCbs.forEach(cb => { + cb(routes); + }); + } + }, + err => { + if (onAbort) { + onAbort(err); + } + if (err && !this.ready) { + this.ready = true; + this.readyErrorCbs.forEach(cb => { + cb(err); + }); + } } - }); + ); } - confirmTransition (route, onComplete, onAbort) { + confirmTransition (routes, onComplete, onAbort) { const current = this.current; const abort = err => { - if (isError(err)) { + // after merging https://github.com/vuejs/vue-router/pull/2771 we + // When the user navigates through history through back/forward buttons + // we do not want to throw the error. We only throw it if directly calling + // push/replace. That's why it's not included in isError + if (!isExtendedError(NavigationDuplicated, err) && isError(err)) { if (this.errorCbs.length) { - this.errorCbs.forEach(cb => { cb(err); }); + this.errorCbs.forEach(cb => { + cb(err); + }); } else { warn(false, 'uncaught error during route navigation:'); console.error(err); @@ -1911,20 +2020,28 @@ class History { } onAbort && onAbort(err); }; - if ( - isSameRoute(route, current) && - // in the case the route map has been dynamically appended to - route.matched.length === current.matched.length - ) { + + let equalLayers = 0; + for (equalLayers = 0; equalLayers < routes.length && equalLayers < current.length; equalLayers++) { + if ( + !isSameRoute(routes[equalLayers], current[equalLayers]) || + // in the case the route map has been dynamically appended to + routes[equalLayers].matched.length !== current[equalLayers].matched.length + ) { + break + } + } + + if (equalLayers === routes.length && equalLayers === current.length) { this.ensureURL(); - return abort() + return abort(new NavigationDuplicated(routes)) } - const { - updated, - deactivated, - activated - } = resolveQueue(this.current.matched, route.matched); + const { updated, deactivated, activated } = resolveQueues( + current, + routes, + equalLayers + ); const queue = [].concat( // in-component leave guards @@ -1939,23 +2056,21 @@ class History { resolveAsyncComponents(activated) ); - this.pending = route; + this.pending = routes; const iterator = (hook, next) => { - if (this.pending !== route) { + if (this.pending !== routes) { return abort() } try { - hook(route, current, (to) => { + hook(routes, current, (to) => { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true); abort(to); } else if ( typeof to === 'string' || - (typeof to === 'object' && ( - typeof to.path === 'string' || - typeof to.name === 'string' - )) + (typeof to === 'object' && + (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({ path: '/' }) -> redirect abort(); @@ -1976,32 +2091,34 @@ class History { runQueue(queue, iterator, () => { const postEnterCbs = []; - const isValid = () => this.current === route; + const isValid = () => this.current === routes; // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); const queue = enterGuards.concat(this.router.resolveHooks); runQueue(queue, iterator, () => { - if (this.pending !== route) { + if (this.pending !== routes) { return abort() } this.pending = null; - onComplete(route); + onComplete(equalLayers); if (this.router.app) { this.router.app.$nextTick(() => { - postEnterCbs.forEach(cb => { cb(); }); + postEnterCbs.forEach(cb => { + cb(); + }); }); } }); }); } - updateRoute (route) { + updateCurrent (routes, equalLayers) { const prev = this.current; - this.current = route; - this.cb && this.cb(route); + this.current = routes; + this.cb && this.cb(routes, equalLayers); this.router.afterHooks.forEach(hook => { - hook && hook(route, prev); + hook && hook(routes, prev); }); } } @@ -2026,6 +2143,33 @@ function normalizeBase (base) { return base.replace(/\/$/, '') } +function resolveQueues ( + current, + next, + equalLayers +) { + const res = { + updated: [], + activated: [], + deactivated: [] + }; + + const min = Math.min(current.length, next.length); + for (let i = equalLayers; i < min; i++) { + const r = resolveQueue(current[i].matched, next[i].matched); + res.updated.push(...r.updated); + res.activated.push(...r.activated); + res.deactivated.push(...r.deactivated); + } + for (let i = min; i < current.length; i++) { + res.deactivated.push(...current[i].matched); + } + for (let i = min; i < next.length; i++) { + res.activated.push(...next[i].matched); + } + return res +} + function resolveQueue ( current, next @@ -2093,9 +2237,13 @@ function extractEnterGuards ( cbs, isValid ) { - return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => { - return bindEnterGuard(guard, match, key, cbs, isValid) - }) + return extractGuards( + activated, + 'beforeRouteEnter', + (guard, _, match, key) => { + return bindEnterGuard(guard, match, key, cbs, isValid) + } + ) } function bindEnterGuard ( @@ -2164,7 +2312,11 @@ class HTML5History extends History { return } - this.transitionTo(location, route => { + let locations = [location]; + if (window.history.state.state) { + locations = window.history.state.state; + } + this.transitionTo(locations, route => { if (supportsScroll) { handleScroll(router, route, current, true); } @@ -2176,28 +2328,51 @@ class HTML5History extends History { window.history.go(n); } - push (location, onComplete, onAbort) { + navigateAllLayers (locations, push, onComplete, onAbort) { const { current: fromRoute } = this; - this.transitionTo(location, route => { - pushState(cleanPath(this.base + route.fullPath)); + this.transitionTo(locations, routes => { + this.ensureURL(push); + const route = this.current[this.current.length - 1]; handleScroll(this.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); } - replace (location, onComplete, onAbort) { - const { current: fromRoute } = this; - this.transitionTo(location, route => { - replaceState(cleanPath(this.base + route.fullPath)); - handleScroll(this.router, route, fromRoute, false); - onComplete && onComplete(route); - }, onAbort); + navigateLastLayer (location, push, onComplete, onAbort) { + const locations = [ + ...this.current.slice(0, -1).map(r => r.fullPath), + location + ]; + this.navigateAllLayers(locations, push, onComplete, onAbort); + } + + navigateLayer (layer, location, push, onComplete, onAbort) { + const locations = [ + ...this.current.slice(0, layer).map(r => r.fullPath), + location, + ...this.current.slice(layer + 1).map(r => r.fullPath) + ]; + this.navigateAllLayers(locations, push, onComplete, onAbort); + } + + navigateAddLayer (location, push, onComplete, onAbort) { + const locations = [ + ...this.current.map(r => r.fullPath), + location + ]; + this.navigateAllLayers(locations, push, onComplete, onAbort); + } + + navigateRemoveLayer (location, push, onComplete, onAbort) { + const locations = this.current.slice(0, -1).map(r => r.fullPath); + this.navigateAllLayers(locations, push, onComplete, onAbort); } ensureURL (push) { - if (getLocation(this.base) !== this.current.fullPath) { - const current = cleanPath(this.base + this.current.fullPath); - push ? pushState(current) : replaceState(current); + const route = this.current[this.current.length - 1]; + if (getLocation(this.base) !== route.fullPath) { + const path = cleanPath(this.base + route.fullPath); + pushState(path, this.current.map(r => r.fullPath), !push); } } @@ -2366,18 +2541,26 @@ class AbstractHistory extends History { } push (location, onComplete, onAbort) { - this.transitionTo(location, route => { - this.stack = this.stack.slice(0, this.index + 1).concat(route); - this.index++; - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + route => { + this.stack = this.stack.slice(0, this.index + 1).concat(route); + this.index++; + onComplete && onComplete(route); + }, + onAbort + ); } replace (location, onComplete, onAbort) { - this.transitionTo(location, route => { - this.stack = this.stack.slice(0, this.index).concat(route); - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + route => { + this.stack = this.stack.slice(0, this.index).concat(route); + onComplete && onComplete(route); + }, + onAbort + ); } go (n) { @@ -2386,10 +2569,18 @@ class AbstractHistory extends History { return } const route = this.stack[targetIndex]; - this.confirmTransition(route, () => { - this.index = targetIndex; - this.updateRoute(route); - }); + this.confirmTransition( + route, + () => { + this.index = targetIndex; + this.updateRoute(route); + }, + err => { + if (isExtendedError(NavigationDuplicated, err)) { + this.index = targetIndex; + } + } + ); } getCurrentLocation () { @@ -2502,7 +2693,7 @@ class VueRouter { const history = this.history; if (history instanceof HTML5History) { - history.transitionTo(history.getCurrentLocation()); + history.transitionTo([history.getCurrentLocation()]); } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners(); @@ -2514,9 +2705,21 @@ class VueRouter { ); } - history.listen(route => { + history.listen((routes, equalLayers) => { this.apps.forEach((app) => { - app._route = route; + // Mutate only the changed routes, so we don't trigger unnecessary updates. + // Changed elements + for (let i = equalLayers; i < app._routes.length && i < routes.length; i++) { + app.$set(app._routes, i, routes[i]); + } + // Added elements + for (let i = app._routes.length; i < routes.length; i++) { + app._routes.push(routes[i]); + } + // Removed + while (app._routes.length > routes.length) { + app._routes.pop(); + } }); }); } @@ -2542,11 +2745,43 @@ class VueRouter { } push (location, onComplete, onAbort) { - this.history.push(location, onComplete, onAbort); + this.history.navigateLastLayer(location, true, onComplete, onAbort); } replace (location, onComplete, onAbort) { - this.history.replace(location, onComplete, onAbort); + this.history.navigateLastLayer(location, false, onComplete, onAbort); + } + + pushAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, true, onComplete, onAbort); + } + + replaceAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, false, onComplete, onAbort); + } + + pushRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(true, onComplete, onAbort); + } + + replaceRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(false, onComplete, onAbort); + } + + pushLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, true, onComplete, onAbort); + } + + replaceLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, false, onComplete, onAbort); + } + + pushAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, true, onComplete, onAbort); + } + + replaceAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, false, onComplete, onAbort); } go (n) { diff --git a/dist/vue-router.esm.browser.min.js b/dist/vue-router.esm.browser.min.js index d8ada1d8b..91bdbeb68 100644 --- a/dist/vue-router.esm.browser.min.js +++ b/dist/vue-router.esm.browser.min.js @@ -3,4 +3,4 @@ * (c) 2019 Evan You * @license MIT */ -function t(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function e(t,e){for(const n in e)t[n]=e[n];return t}var n={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"}},render(t,{props:n,children:r,parent:o,data:i}){i.routerView=!0;const s=o.$createElement,a=n.name,c=o.$route,u=o._routerViewCache||(o._routerViewCache={});let h=0,p=!1;for(;o&&o._routerRoot!==o;){const t=o.$vnode&&o.$vnode.data;t&&(t.routerView&&h++,t.keepAlive&&o._inactive&&(p=!0)),o=o.$parent}if(i.routerViewDepth=h,p)return s(u[a],i,r);const l=c.matched[h];if(!l)return u[a]=null,s();const f=u[a]=l.components[a];i.registerRouteInstance=(t,e)=>{const n=l.instances[a];(e&&n!==t||!e&&n===t)&&(l.instances[a]=e)},(i.hook||(i.hook={})).prepatch=(t,e)=>{l.instances[a]=e.componentInstance},i.hook.init=t=>{t.data.keepAlive&&t.componentInstance&&t.componentInstance!==l.instances[a]&&(l.instances[a]=t.componentInstance)};let d=i.props=function(t,e){switch(typeof e){case"undefined":return;case"object":return e;case"function":return e(t);case"boolean":return e?t.params:void 0}}(c,l.props&&l.props[a]);if(d){d=i.props=e({},d);const t=i.attrs=i.attrs||{};for(const e in d)f.props&&e in f.props||(t[e]=d[e],delete d[e])}return s(f,i,r)}};const r=/[!'()*]/g,o=t=>"%"+t.charCodeAt(0).toString(16),i=/%2C/g,s=t=>encodeURIComponent(t).replace(r,o).replace(i,","),a=decodeURIComponent;function c(t){const e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach(t=>{const n=t.replace(/\+/g," ").split("="),r=a(n.shift()),o=n.length>0?a(n.join("=")):null;void 0===e[r]?e[r]=o:Array.isArray(e[r])?e[r].push(o):e[r]=[e[r],o]}),e):e}function u(t){const e=t?Object.keys(t).map(e=>{const n=t[e];if(void 0===n)return"";if(null===n)return s(e);if(Array.isArray(n)){const t=[];return n.forEach(n=>{void 0!==n&&(null===n?t.push(s(e)):t.push(s(e)+"="+s(n)))}),t.join("&")}return s(e)+"="+s(n)}).filter(t=>t.length>0).join("&"):null;return e?`?${e}`:""}const h=/\/?$/;function p(t,e,n,r){const o=r&&r.options.stringifyQuery;let i=e.query||{};try{i=l(i)}catch(t){}const s={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:i,params:e.params||{},fullPath:y(e,o),matched:t?d(t):[]};return n&&(s.redirectedFrom=y(n,o)),Object.freeze(s)}function l(t){if(Array.isArray(t))return t.map(l);if(t&&"object"==typeof t){const e={};for(const n in t)e[n]=l(t[n]);return e}return t}const f=p(null,{path:"/"});function d(t){const e=[];for(;t;)e.unshift(t),t=t.parent;return e}function y({path:t,query:e={},hash:n=""},r){return(t||"/")+(r||u)(e)+n}function m(t,e){return e===f?t===e:!!e&&(t.path&&e.path?t.path.replace(h,"")===e.path.replace(h,"")&&t.hash===e.hash&&g(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&g(t.query,e.query)&&g(t.params,e.params)))}function g(t={},e={}){if(!t||!e)return t===e;const n=Object.keys(t),r=Object.keys(e);return n.length===r.length&&n.every(n=>{const r=t[n],o=e[n];return"object"==typeof r&&"object"==typeof o?g(r,o):String(r)===String(o)})}const w=[String,Object],b=[String,Array];var v={name:"RouterLink",props:{to:{type:w,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,event:{type:b,default:"click"}},render(t){const n=this.$router,r=this.$route,{location:o,route:i,href:s}=n.resolve(this.to,r,this.append),a={},c=n.options.linkActiveClass,u=n.options.linkExactActiveClass,l=null==c?"router-link-active":c,f=null==u?"router-link-exact-active":u,d=null==this.activeClass?l:this.activeClass,y=null==this.exactActiveClass?f:this.exactActiveClass,g=o.path?p(null,o,null,n):i;a[y]=m(r,g),a[d]=this.exact?a[y]:function(t,e){return 0===t.path.replace(h,"/").indexOf(e.path.replace(h,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(const n in e)if(!(n in t))return!1;return!0}(t.query,e.query)}(r,g);const w=t=>{x(t)&&(this.replace?n.replace(o):n.push(o))},b={click:x};Array.isArray(this.event)?this.event.forEach(t=>{b[t]=w}):b[this.event]=w;const v={class:a};if("a"===this.tag)v.on=b,v.attrs={href:s};else{const t=function t(e){if(e){let n;for(let r=0;r{!function t(e,n,r,o,i,s){const{path:a,name:c}=o;const u=o.pathToRegexpOptions||{};const h=function(t,e,n){n||(t=t.replace(/\/$/,""));return"/"===t[0]?t:null==e?t:O(`${e.path}/${t}`)}(a,i,u.strict);"boolean"==typeof o.caseSensitive&&(u.sensitive=o.caseSensitive);const p={path:h,regex:K(h,u),components:o.components||{default:o.component},instances:{},name:c,parent:i,matchAs:s,redirect:o.redirect,beforeEnter:o.beforeEnter,meta:o.meta||{},props:null==o.props?{}:o.components?o.props:{default:o.props}};o.children&&o.children.forEach(o=>{const i=s?O(`${s}/${o.path}`):void 0;t(e,n,r,o,p,i)});if(void 0!==o.alias){const s=Array.isArray(o.alias)?o.alias:[o.alias];s.forEach(s=>{const a={path:s,children:o.children};t(e,n,r,a,i,p.path||"/")})}n[p.path]||(e.push(p.path),n[p.path]=p);c&&(r[c]||(r[c]=p))}(o,i,s,t)});for(let t=0,e=o.length;t=0&&(e=t.slice(r),t=t.slice(0,r));const o=t.indexOf("?");return o>=0&&(n=t.slice(o+1),t=t.slice(0,o)),{path:t,query:n,hash:e}}(i.path||""),a=n&&n.path||"/",u=s.path?E(s.path,a,r||i.append):a,h=function(t,e={},n){const r=n||c;let o;try{o=r(t||"")}catch(t){o={}}for(const t in e)o[t]=e[t];return o}(s.query,i.query,o&&o.options.parseQuery);let p=i.hash||s.hash;return p&&"#"!==p.charAt(0)&&(p=`#${p}`),{_normalized:!0,path:u,query:h,hash:p}}function N(t,e){const{pathList:n,pathMap:r,nameMap:o}=F(t);function i(t,i,s){const c=J(t,i,!1,e),{name:u}=c;if(u){const t=o[u];if(!t)return a(null,c);const e=t.regex.keys.filter(t=>!t.optional).map(t=>t.name);if("object"!=typeof c.params&&(c.params={}),i&&"object"==typeof i.params)for(const t in i.params)!(t in c.params)&&e.indexOf(t)>-1&&(c.params[t]=i.params[t]);return c.path=D(t.path,c.params),a(t,c,s)}if(c.path){c.params={};for(let t=0;t{G(),t.state&&t.state.key&&function(t){it=t}(t.state.key)})}function W(t,e,n,r){if(!t.app)return;const o=t.options.scrollBehavior;o&&t.app.$nextTick(()=>{const i=function(){const t=at();if(t)return X[t]}(),s=o.call(t,e,n,r?i:null);s&&("function"==typeof s.then?s.then(t=>{nt(t,i)}).catch(t=>{}):nt(s,i))})}function G(){const t=at();t&&(X[t]={x:window.pageXOffset,y:window.pageYOffset})}function Z(t){return et(t.x)||et(t.y)}function tt(t){return{x:et(t.x)?t.x:window.pageXOffset,y:et(t.y)?t.y:window.pageYOffset}}function et(t){return"number"==typeof t}function nt(t,e){const n="object"==typeof t;if(n&&"string"==typeof t.selector){const n=document.querySelector(t.selector);if(n){let o=t.offset&&"object"==typeof t.offset?t.offset:{};e=function(t,e){const n=document.documentElement.getBoundingClientRect(),r=t.getBoundingClientRect();return{x:r.left-n.left-e.x,y:r.top-n.top-e.y}}(n,o={x:et((r=o).x)?r.x:0,y:et(r.y)?r.y:0})}else Z(t)&&(e=tt(t))}else n&&Z(t)&&(e=tt(t));var r;e&&window.scrollTo(e.x,e.y)}const rt=R&&function(){const t=window.navigator.userAgent;return(-1===t.indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&(window.history&&"pushState"in window.history)}(),ot=R&&window.performance&&window.performance.now?window.performance:Date;let it=st();function st(){return ot.now().toFixed(3)}function at(){return it}function ct(t,e){G();const n=window.history;try{e?n.replaceState({key:it},"",t):(it=st(),n.pushState({key:it},"",t))}catch(n){window.location[e?"replace":"assign"](t)}}function ut(t){ct(t,!0)}function ht(t,e,n){const r=o=>{o>=t.length?n():t[o]?e(t[o],()=>{r(o+1)}):r(o+1)};r(0)}function pt(e){return(n,r,o)=>{let i=!1,s=0,a=null;lt(e,(e,n,r,c)=>{if("function"==typeof e&&void 0===e.cid){i=!0,s++;const n=yt(t=>{(function(t){return t.__esModule||dt&&"Module"===t[Symbol.toStringTag]})(t)&&(t=t.default),e.resolved="function"==typeof t?t:k.extend(t),r.components[c]=t,--s<=0&&o()}),u=yt(e=>{const n=`Failed to resolve async component ${c}: ${e}`;a||(a=t(e)?e:new Error(n),o(a))});let h;try{h=e(n,u)}catch(t){u(t)}if(h)if("function"==typeof h.then)h.then(n,u);else{const t=h.component;t&&"function"==typeof t.then&&t.then(n,u)}}}),i||o()}}function lt(t,e){return ft(t.map(t=>Object.keys(t.components).map(n=>e(t.components[n],t.instances[n],t,n))))}function ft(t){return Array.prototype.concat.apply([],t)}const dt="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function yt(t){let e=!1;return function(...n){if(!e)return e=!0,t.apply(this,n)}}class mt{constructor(t,e){this.router=t,this.base=function(t){if(!t)if(R){const e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";"/"!==t.charAt(0)&&(t="/"+t);return t.replace(/\/$/,"")}(e),this.current=f,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]}listen(t){this.cb=t}onReady(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))}onError(t){this.errorCbs.push(t)}transitionTo(t,e,n){const r=this.router.match(t,this.current);this.confirmTransition(r,()=>{this.updateRoute(r),e&&e(r),this.ensureURL(),this.ready||(this.ready=!0,this.readyCbs.forEach(t=>{t(r)}))},t=>{n&&n(t),t&&!this.ready&&(this.ready=!0,this.readyErrorCbs.forEach(e=>{e(t)}))})}confirmTransition(e,n,r){const o=this.current,i=e=>{t(e)&&(this.errorCbs.length?this.errorCbs.forEach(t=>{t(e)}):console.error(e)),r&&r(e)};if(m(e,o)&&e.matched.length===o.matched.length)return this.ensureURL(),i();const{updated:s,deactivated:a,activated:c}=function(t,e){let n;const r=Math.max(t.length,e.length);for(n=0;nt.beforeEnter),pt(c));this.pending=e;const h=(n,r)=>{if(this.pending!==e)return i();try{n(e,o,e=>{!1===e||t(e)?(this.ensureURL(!0),i(e)):"string"==typeof e||"object"==typeof e&&("string"==typeof e.path||"string"==typeof e.name)?(i(),"object"==typeof e&&e.replace?this.replace(e):this.push(e)):r(e)})}catch(t){i(t)}};ht(u,h,()=>{const t=[];ht(function(t,e,n){return gt(t,"beforeRouteEnter",(t,r,o,i)=>(function(t,e,n,r,o){return function(i,s,a){return t(i,s,t=>{"function"==typeof t&&r.push(()=>{!function t(e,n,r,o){n[r]&&!n[r]._isBeingDestroyed?e(n[r]):o()&&setTimeout(()=>{t(e,n,r,o)},16)}(t,e.instances,n,o)}),a(t)})}})(t,o,i,e,n))}(c,t,()=>this.current===e).concat(this.router.resolveHooks),h,()=>{if(this.pending!==e)return i();this.pending=null,n(e),this.router.app&&this.router.app.$nextTick(()=>{t.forEach(t=>{t()})})})})}updateRoute(t){const e=this.current;this.current=t,this.cb&&this.cb(t),this.router.afterHooks.forEach(n=>{n&&n(t,e)})}}function gt(t,e,n,r){const o=lt(t,(t,r,o,i)=>{const s=function(t,e){"function"!=typeof t&&(t=k.extend(t));return t.options[e]}(t,e);if(s)return Array.isArray(s)?s.map(t=>n(t,r,o,i)):n(s,r,o,i)});return ft(r?o.reverse():o)}function wt(t,e){if(e)return function(){return t.apply(e,arguments)}}class bt extends mt{constructor(t,e){super(t,e);const n=t.options.scrollBehavior,r=rt&&n;r&&Y();const o=vt(this.base);window.addEventListener("popstate",e=>{const n=this.current,i=vt(this.base);this.current===f&&i===o||this.transitionTo(i,e=>{r&&W(t,e,n,!0)})})}go(t){window.history.go(t)}push(t,e,n){const{current:r}=this;this.transitionTo(t,t=>{ct(O(this.base+t.fullPath)),W(this.router,t,r,!1),e&&e(t)},n)}replace(t,e,n){const{current:r}=this;this.transitionTo(t,t=>{ut(O(this.base+t.fullPath)),W(this.router,t,r,!1),e&&e(t)},n)}ensureURL(t){if(vt(this.base)!==this.current.fullPath){const e=O(this.base+this.current.fullPath);t?ct(e):ut(e)}}getCurrentLocation(){return vt(this.base)}}function vt(t){let e=decodeURI(window.location.pathname);return t&&0===e.indexOf(t)&&(e=e.slice(t.length)),(e||"/")+window.location.search+window.location.hash}class xt extends mt{constructor(t,e,n){super(t,e),n&&function(t){const e=vt(t);if(!/^\/#/.test(e))return window.location.replace(O(t+"/#"+e)),!0}(this.base)||kt()}setupListeners(){const t=this.router.options.scrollBehavior,e=rt&&t;e&&Y(),window.addEventListener(rt?"popstate":"hashchange",()=>{const t=this.current;kt()&&this.transitionTo(Rt(),n=>{e&&W(this.router,n,t,!0),rt||At(n.fullPath)})})}push(t,e,n){const{current:r}=this;this.transitionTo(t,t=>{Ot(t.fullPath),W(this.router,t,r,!1),e&&e(t)},n)}replace(t,e,n){const{current:r}=this;this.transitionTo(t,t=>{At(t.fullPath),W(this.router,t,r,!1),e&&e(t)},n)}go(t){window.history.go(t)}ensureURL(t){const e=this.current.fullPath;Rt()!==e&&(t?Ot(e):At(e))}getCurrentLocation(){return Rt()}}function kt(){const t=Rt();return"/"===t.charAt(0)||(At("/"+t),!1)}function Rt(){let t=window.location.href;const e=t.indexOf("#");if(e<0)return"";const n=(t=t.slice(e+1)).indexOf("?");if(n<0){const e=t.indexOf("#");t=e>-1?decodeURI(t.slice(0,e))+t.slice(e):decodeURI(t)}else n>-1&&(t=decodeURI(t.slice(0,n))+t.slice(n));return t}function Et(t){const e=window.location.href,n=e.indexOf("#");return`${n>=0?e.slice(0,n):e}#${t}`}function Ot(t){rt?ct(Et(t)):window.location.hash=t}function At(t){rt?ut(Et(t)):window.location.replace(Et(t))}class Ct extends mt{constructor(t,e){super(t,e),this.stack=[],this.index=-1}push(t,e,n){this.transitionTo(t,t=>{this.stack=this.stack.slice(0,this.index+1).concat(t),this.index++,e&&e(t)},n)}replace(t,e,n){this.transitionTo(t,t=>{this.stack=this.stack.slice(0,this.index).concat(t),e&&e(t)},n)}go(t){const e=this.index+t;if(e<0||e>=this.stack.length)return;const n=this.stack[e];this.confirmTransition(n,()=>{this.index=e,this.updateRoute(n)})}getCurrentLocation(){const t=this.stack[this.stack.length-1];return t?t.fullPath:"/"}ensureURL(){}}class $t{constructor(t={}){this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=N(t.routes||[],this);let e=t.mode||"hash";switch(this.fallback="history"===e&&!rt&&!1!==t.fallback,this.fallback&&(e="hash"),R||(e="abstract"),this.mode=e,e){case"history":this.history=new bt(this,t.base);break;case"hash":this.history=new xt(this,t.base,this.fallback);break;case"abstract":this.history=new Ct(this,t.base)}}match(t,e,n){return this.matcher.match(t,e,n)}get currentRoute(){return this.history&&this.history.current}init(t){if(this.apps.push(t),t.$once("hook:destroyed",()=>{const e=this.apps.indexOf(t);e>-1&&this.apps.splice(e,1),this.app===t&&(this.app=this.apps[0]||null)}),this.app)return;this.app=t;const e=this.history;if(e instanceof bt)e.transitionTo(e.getCurrentLocation());else if(e instanceof xt){const t=()=>{e.setupListeners()};e.transitionTo(e.getCurrentLocation(),t,t)}e.listen(t=>{this.apps.forEach(e=>{e._route=t})})}beforeEach(t){return jt(this.beforeHooks,t)}beforeResolve(t){return jt(this.resolveHooks,t)}afterEach(t){return jt(this.afterHooks,t)}onReady(t,e){this.history.onReady(t,e)}onError(t){this.history.onError(t)}push(t,e,n){this.history.push(t,e,n)}replace(t,e,n){this.history.replace(t,e,n)}go(t){this.history.go(t)}back(){this.go(-1)}forward(){this.go(1)}getMatchedComponents(t){const e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map(t=>Object.keys(t.components).map(e=>t.components[e]))):[]}resolve(t,e,n){const r=J(t,e=e||this.history.current,n,this),o=this.match(r,e),i=o.redirectedFrom||o.fullPath;return{location:r,route:o,href:function(t,e,n){var r="hash"===n?"#"+e:e;return t?O(t+"/"+r):r}(this.history.base,i,this.mode),normalizedTo:r,resolved:o}}addRoutes(t){this.matcher.addRoutes(t),this.history.current!==f&&this.history.transitionTo(this.history.getCurrentLocation())}}function jt(t,e){return t.push(e),()=>{const n=t.indexOf(e);n>-1&&t.splice(n,1)}}$t.install=function t(e){if(t.installed&&k===e)return;t.installed=!0,k=e;const r=t=>void 0!==t,o=(t,e)=>{let n=t.$options._parentVnode;r(n)&&r(n=n.data)&&r(n=n.registerRouteInstance)&&n(t,e)};e.mixin({beforeCreate(){r(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),e.util.defineReactive(this,"_route",this._router.history.current)):this._routerRoot=this.$parent&&this.$parent._routerRoot||this,o(this,this)},destroyed(){o(this)}}),Object.defineProperty(e.prototype,"$router",{get(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$route",{get(){return this._routerRoot._route}}),e.component("RouterView",n),e.component("RouterLink",v);const i=e.config.optionMergeStrategies;i.beforeRouteEnter=i.beforeRouteLeave=i.beforeRouteUpdate=i.created},$t.version="3.0.7",R&&window.Vue&&window.Vue.use($t);export default $t; \ No newline at end of file +function t(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function e(t,e){return e instanceof t||e&&e.name===t.name}function r(t,e){for(const r in e)t[r]=e[r];return t}var n={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"},nextLayer:{type:Boolean,default:!1}},render(t,{props:e,children:n,parent:o,data:s}){s.routerView=!0;const i=o.$createElement,a=e.name,c=o._routerViewCache||(o._routerViewCache={}),h=o._routerLayer+(e.nextLayer?1:0);if(o._routerRoot._routes.length<=h)return c[a]=null,i();const u=o._routerRoot._routes[h];let l=0,p=!1;for(;o&&o._routerRoot!==o;){const t=o.$vnode&&o.$vnode.data;t&&(t.routerView&&l++,t.keepAlive&&o._inactive&&(p=!0)),o=o.$parent}if(s.routerViewDepth=l,p)return i(c[a],s,n);const f=u.matched[l];if(!f)return c[a]=null,i();const d=c[a]=f.components[a];s.registerRouteInstance=(t,e)=>{const r=f.instances[a];(e&&r!==t||!e&&r===t)&&(f.instances[a]=e),t._routerLayer=h},(s.hook||(s.hook={})).prepatch=(t,e)=>{f.instances[a]=e.componentInstance},s.hook.init=t=>{t.data.keepAlive&&t.componentInstance&&t.componentInstance!==f.instances[a]&&(f.instances[a]=t.componentInstance)};let y=s.props=function(t,e){switch(typeof e){case"undefined":return;case"object":return e;case"function":return e(t);case"boolean":return e?t.params:void 0}}(u,f.props&&f.props[a]);if(y){y=s.props=r({},y);const t=s.attrs=s.attrs||{};for(const e in y)d.props&&e in d.props||(t[e]=y[e],delete y[e])}return i(d,s,n)}};const o=/[!'()*]/g,s=t=>"%"+t.charCodeAt(0).toString(16),i=/%2C/g,a=t=>encodeURIComponent(t).replace(o,s).replace(i,","),c=decodeURIComponent;function h(t){const e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach(t=>{const r=t.replace(/\+/g," ").split("="),n=c(r.shift()),o=r.length>0?c(r.join("=")):null;void 0===e[n]?e[n]=o:Array.isArray(e[n])?e[n].push(o):e[n]=[e[n],o]}),e):e}function u(t){const e=t?Object.keys(t).map(e=>{const r=t[e];if(void 0===r)return"";if(null===r)return a(e);if(Array.isArray(r)){const t=[];return r.forEach(r=>{void 0!==r&&(null===r?t.push(a(e)):t.push(a(e)+"="+a(r)))}),t.join("&")}return a(e)+"="+a(r)}).filter(t=>t.length>0).join("&"):null;return e?`?${e}`:""}const l=/\/?$/;function p(t,e,r,n){const o=n&&n.options.stringifyQuery;let s=e.query||{};try{s=f(s)}catch(t){}const i={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:s,params:e.params||{},fullPath:m(e,o),matched:t?y(t):[]};return r&&(i.redirectedFrom=m(r,o)),Object.freeze(i)}function f(t){if(Array.isArray(t))return t.map(f);if(t&&"object"==typeof t){const e={};for(const r in t)e[r]=f(t[r]);return e}return t}const d=p(null,{path:"/"});function y(t){const e=[];for(;t;)e.unshift(t),t=t.parent;return e}function m({path:t,query:e={},hash:r=""},n){return(t||"/")+(n||u)(e)+r}function g(t,e){return e===d?t===e:!!e&&(t.path&&e.path?t.path.replace(l,"")===e.path.replace(l,"")&&t.hash===e.hash&&v(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&v(t.query,e.query)&&v(t.params,e.params)))}function v(t={},e={}){if(!t||!e)return t===e;const r=Object.keys(t),n=Object.keys(e);return r.length===n.length&&r.every(r=>{const n=t[r],o=e[r];return"object"==typeof n&&"object"==typeof o?v(n,o):String(n)===String(o)})}function w(t,e,r){const n=t.charAt(0);if("/"===n)return t;if("?"===n||"#"===n)return e+t;const o=e.split("/");r&&o[o.length-1]||o.pop();const s=t.replace(/^\//,"").split("/");for(let t=0;t=0&&(e=t.slice(n),t=t.slice(0,n));const o=t.indexOf("?");return o>=0&&(r=t.slice(o+1),t=t.slice(0,o)),{path:t,query:r,hash:e}}(s.path||""),a=e&&e.path||"/",c=i.path?w(i.path,a,n||s.append):a,u=function(t,e={},r){const n=r||h;let o;try{o=n(t||"")}catch(t){o={}}for(const t in e)o[t]=e[t];return o}(i.query,s.query,o&&o.options.parseQuery);let l=s.hash||i.hash;return l&&"#"!==l.charAt(0)&&(l=`#${l}`),{_normalized:!0,path:c,query:u,hash:l}}const V=[String,Object],H=[String,Array];var z={name:"RouterLink",props:{to:{type:V,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,addLayer:Boolean,removeLayer:Boolean,activeClass:String,exactActiveClass:String,event:{type:H,default:"click"}},render(t){const e=this.$router,n=this.$route,{location:o,route:s,href:i}=e.resolve(this.to,n,this.append),a={},c=e.options.linkActiveClass,h=e.options.linkExactActiveClass,u=null==c?"router-link-active":c,f=null==h?"router-link-exact-active":h,d=null==this.activeClass?u:this.activeClass,y=null==this.exactActiveClass?f:this.exactActiveClass,m=s.redirectedFrom?p(null,B(s.redirectedFrom),null,e):s;a[y]=g(n,m),a[d]=this.exact?a[y]:function(t,e){return 0===t.path.replace(l,"/").indexOf(e.path.replace(l,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(const r in e)if(!(r in t))return!1;return!0}(t.query,e.query)}(n,m);const v=t=>{F(t)&&(this.replace?this.addLayer?e.replaceAddLayer(o):this.removeLayer?e.replaceRemoveLayer():e.replaceLayer(this._routerLayer,o):this.addLayer?e.pushAddLayer(o):this.removeLayer?e.pushRemoveLayer():e.pushLayer(this._routerLayer,o))},w={click:F};Array.isArray(this.event)?this.event.forEach(t=>{w[t]=v}):w[this.event]=v;const b={class:a};if("a"===this.tag)b.on=w,b.attrs={href:i};else{const t=function t(e){if(e){let r;for(let n=0;n{!function t(e,r,n,o,s,i){const{path:a,name:c}=o;const h=o.pathToRegexpOptions||{};const u=function(t,e,r){r||(t=t.replace(/\/$/,""));return"/"===t[0]?t:null==e?t:b(`${e.path}/${t}`)}(a,s,h.strict);"boolean"==typeof o.caseSensitive&&(h.sensitive=o.caseSensitive);const l={path:u,regex:Q(u,h),components:o.components||{default:o.component},instances:{},name:c,parent:s,matchAs:i,redirect:o.redirect,beforeEnter:o.beforeEnter,meta:o.meta||{},props:null==o.props?{}:o.components?o.props:{default:o.props}};o.children&&o.children.forEach(o=>{const s=i?b(`${i}/${o.path}`):void 0;t(e,r,n,o,l,s)});if(void 0!==o.alias){const i=Array.isArray(o.alias)?o.alias:[o.alias];i.forEach(i=>{const a={path:i,children:o.children};t(e,r,n,a,s,l.path||"/")})}r[l.path]||(e.push(l.path),r[l.path]=l);c&&(n[c]||(n[c]=l))}(o,s,i,t)});for(let t=0,e=o.length;t!t.optional).map(t=>t.name);if("object"!=typeof c.params&&(c.params={}),s&&"object"==typeof s.params)for(const t in s.params)!(t in c.params)&&e.indexOf(t)>-1&&(c.params[t]=s.params[t]);return c.path=M(t.path,c.params),a(t,c,i)}if(c.path){c.params={};for(let t=0;t{tt(),t.state&&t.state.key&&function(t){ct=t}(t.state.key)})}function Z(t,e,r,n){if(!t.app)return;const o=t.options.scrollBehavior;o&&t.app.$nextTick(()=>{const s=function(){const t=ut();if(t)return W[t]}(),i=o.call(t,e,r,n?s:null);i&&("function"==typeof i.then?i.then(t=>{st(t,s)}).catch(t=>{}):st(i,s))})}function tt(){const t=ut();t&&(W[t]={x:window.pageXOffset,y:window.pageYOffset})}function et(t){return nt(t.x)||nt(t.y)}function rt(t){return{x:nt(t.x)?t.x:window.pageXOffset,y:nt(t.y)?t.y:window.pageYOffset}}function nt(t){return"number"==typeof t}const ot=/^#\d/;function st(t,e){const r="object"==typeof t;if(r&&"string"==typeof t.selector){const r=ot.test(t.selector)?document.getElementById(t.selector.slice(1)):document.querySelector(t.selector);if(r){let o=t.offset&&"object"==typeof t.offset?t.offset:{};e=function(t,e){const r=document.documentElement.getBoundingClientRect(),n=t.getBoundingClientRect();return{x:n.left-r.left-e.x,y:n.top-r.top-e.y}}(r,o={x:nt((n=o).x)?n.x:0,y:nt(n.y)?n.y:0})}else et(t)&&(e=rt(t))}else r&&et(t)&&(e=rt(t));var n;e&&window.scrollTo(e.x,e.y)}const it=N&&function(){const t=window.navigator.userAgent;return(-1===t.indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&(window.history&&"pushState"in window.history)}(),at=N&&window.performance&&window.performance.now?window.performance:Date;let ct=ht();function ht(){return at.now().toFixed(3)}function ut(){return ct}function lt(t,e,r){tt();const n=window.history;try{r?n.replaceState({key:ct,state:e},"",t):(ct=ht(),n.pushState({key:ct,state:e},"",t))}catch(e){window.location[r?"replace":"assign"](t)}}function pt(t,e,r){const n=o=>{o>=t.length?r():t[o]?e(t[o],()=>{n(o+1)}):n(o+1)};n(0)}function ft(e){return(r,n,o)=>{let s=!1,i=0,a=null;dt(e,(e,r,n,c)=>{if("function"==typeof e&&void 0===e.cid){s=!0,i++;const r=gt(t=>{(function(t){return t.__esModule||mt&&"Module"===t[Symbol.toStringTag]})(t)&&(t=t.default),e.resolved="function"==typeof t?t:D.extend(t),n.components[c]=t,--i<=0&&o()}),h=gt(e=>{const r=`Failed to resolve async component ${c}: ${e}`;a||(a=t(e)?e:new Error(r),o(a))});let u;try{u=e(r,h)}catch(t){h(t)}if(u)if("function"==typeof u.then)u.then(r,h);else{const t=u.component;t&&"function"==typeof t.then&&t.then(r,h)}}}),s||o()}}function dt(t,e){return yt(t.map(t=>Object.keys(t.components).map(r=>e(t.components[r],t.instances[r],t,r))))}function yt(t){return Array.prototype.concat.apply([],t)}const mt="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function gt(t){let e=!1;return function(...r){if(!e)return e=!0,t.apply(this,r)}}class vt extends Error{constructor(){super("Navigating to current location is not allowed"),this.name="NavigationDuplicated"}}class wt{constructor(t,e){this.router=t,this.base=function(t){if(!t)if(N){const e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";"/"!==t.charAt(0)&&(t="/"+t);return t.replace(/\/$/,"")}(e),this.current=[d],this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]}listen(t){this.cb=t}onReady(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))}onError(t){this.errorCbs.push(t)}getClosestCurrent(t){return t>=this.current.length?this.current[this.current.length-1]:this.current[t]}transitionTo(t,e,r){const n=t.map((t,e)=>this.router.match(t,this.getClosestCurrent(e)));this.confirmTransition(n,t=>{this.updateCurrent(n,t),e&&e(n),this.ensureURL(),this.ready||(this.ready=!0,this.readyCbs.forEach(t=>{t(n)}))},t=>{r&&r(t),t&&!this.ready&&(this.ready=!0,this.readyErrorCbs.forEach(e=>{e(t)}))})}confirmTransition(r,n,o){const s=this.current,i=r=>{!e(vt,r)&&t(r)&&(this.errorCbs.length?this.errorCbs.forEach(t=>{t(r)}):console.error(r)),o&&o(r)};let a=0;for(a=0;at.beforeEnter),ft(u));this.pending=r;const p=(e,n)=>{if(this.pending!==r)return i();try{e(r,s,e=>{!1===e||t(e)?(this.ensureURL(!0),i(e)):"string"==typeof e||"object"==typeof e&&("string"==typeof e.path||"string"==typeof e.name)?(i(),"object"==typeof e&&e.replace?this.replace(e):this.push(e)):n(e)})}catch(t){i(t)}};pt(l,p,()=>{const t=[];pt(function(t,e,r){return Lt(t,"beforeRouteEnter",(t,n,o,s)=>(function(t,e,r,n,o){return function(s,i,a){return t(s,i,t=>{"function"==typeof t&&n.push(()=>{!function t(e,r,n,o){r[n]&&!r[n]._isBeingDestroyed?e(r[n]):o()&&setTimeout(()=>{t(e,r,n,o)},16)}(t,e.instances,r,o)}),a(t)})}})(t,o,s,e,r))}(u,t,()=>this.current===r).concat(this.router.resolveHooks),p,()=>{if(this.pending!==r)return i();this.pending=null,n(a),this.router.app&&this.router.app.$nextTick(()=>{t.forEach(t=>{t()})})})})}updateCurrent(t,e){const r=this.current;this.current=t,this.cb&&this.cb(t,e),this.router.afterHooks.forEach(e=>{e&&e(t,r)})}}function bt(t,e){let r;const n=Math.max(t.length,e.length);for(r=0;r{const i=function(t,e){"function"!=typeof t&&(t=D.extend(t));return t.options[e]}(t,e);if(i)return Array.isArray(i)?i.map(t=>r(t,n,o,s)):r(i,n,o,s)});return yt(n?o.reverse():o)}function xt(t,e){if(e)return function(){return t.apply(e,arguments)}}class Rt extends wt{constructor(t,e){super(t,e);const r=t.options.scrollBehavior,n=it&&r;n&&G();const o=kt(this.base);window.addEventListener("popstate",e=>{const r=this.current,s=kt(this.base);if(this.current===d&&s===o)return;let i=[s];window.history.state.state&&(i=window.history.state.state),this.transitionTo(i,e=>{n&&Z(t,e,r,!0)})})}go(t){window.history.go(t)}navigateAllLayers(t,e,r,n){const{current:o}=this;this.transitionTo(t,t=>{this.ensureURL(e);const n=this.current[this.current.length-1];Z(this.router,n,o,!1),r&&r(n)},n)}navigateLastLayer(t,e,r,n){const o=[...this.current.slice(0,-1).map(t=>t.fullPath),t];this.navigateAllLayers(o,e,r,n)}navigateLayer(t,e,r,n,o){const s=[...this.current.slice(0,t).map(t=>t.fullPath),e,...this.current.slice(t+1).map(t=>t.fullPath)];this.navigateAllLayers(s,r,n,o)}navigateAddLayer(t,e,r,n){const o=[...this.current.map(t=>t.fullPath),t];this.navigateAllLayers(o,e,r,n)}navigateRemoveLayer(t,e,r,n){const o=this.current.slice(0,-1).map(t=>t.fullPath);this.navigateAllLayers(o,e,r,n)}ensureURL(t){const e=this.current[this.current.length-1];if(kt(this.base)!==e.fullPath){lt(b(this.base+e.fullPath),this.current.map(t=>t.fullPath),!t)}}getCurrentLocation(){return kt(this.base)}}function kt(t){let e=decodeURI(window.location.pathname);return t&&0===e.indexOf(t)&&(e=e.slice(t.length)),(e||"/")+window.location.search+window.location.hash}class At extends wt{constructor(t,e,r){super(t,e),r&&function(t){const e=kt(t);if(!/^\/#/.test(e))return window.location.replace(b(t+"/#"+e)),!0}(this.base)||Et()}setupListeners(){const t=this.router.options.scrollBehavior,e=it&&t;e&&G(),window.addEventListener(it?"popstate":"hashchange",()=>{const t=this.current;Et()&&this.transitionTo(Ct(),r=>{e&&Z(this.router,r,t,!0),it||$t(r.fullPath)})})}push(t,e,r){const{current:n}=this;this.transitionTo(t,t=>{Ot(t.fullPath),Z(this.router,t,n,!1),e&&e(t)},r)}replace(t,e,r){const{current:n}=this;this.transitionTo(t,t=>{$t(t.fullPath),Z(this.router,t,n,!1),e&&e(t)},r)}go(t){window.history.go(t)}ensureURL(t){const e=this.current.fullPath;Ct()!==e&&(t?Ot(e):$t(e))}getCurrentLocation(){return Ct()}}function Et(){const t=Ct();return"/"===t.charAt(0)||($t("/"+t),!1)}function Ct(){let t=window.location.href;const e=t.indexOf("#");if(e<0)return"";const r=(t=t.slice(e+1)).indexOf("?");if(r<0){const e=t.indexOf("#");t=e>-1?decodeURI(t.slice(0,e))+t.slice(e):decodeURI(t)}else r>-1&&(t=decodeURI(t.slice(0,r))+t.slice(r));return t}function _t(t){const e=window.location.href,r=e.indexOf("#");return`${r>=0?e.slice(0,r):e}#${t}`}function Ot(t){it?lt(_t(t)):window.location.hash=t}function $t(t){it?lt(_t(t),!0):window.location.replace(_t(t))}class jt extends wt{constructor(t,e){super(t,e),this.stack=[],this.index=-1}push(t,e,r){this.transitionTo(t,t=>{this.stack=this.stack.slice(0,this.index+1).concat(t),this.index++,e&&e(t)},r)}replace(t,e,r){this.transitionTo(t,t=>{this.stack=this.stack.slice(0,this.index).concat(t),e&&e(t)},r)}go(t){const r=this.index+t;if(r<0||r>=this.stack.length)return;const n=this.stack[r];this.confirmTransition(n,()=>{this.index=r,this.updateRoute(n)},t=>{e(vt,t)&&(this.index=r)})}getCurrentLocation(){const t=this.stack[this.stack.length-1];return t?t.fullPath:"/"}ensureURL(){}}class Tt{constructor(t={}){this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=X(t.routes||[],this);let e=t.mode||"hash";switch(this.fallback="history"===e&&!it&&!1!==t.fallback,this.fallback&&(e="hash"),N||(e="abstract"),this.mode=e,e){case"history":this.history=new Rt(this,t.base);break;case"hash":this.history=new At(this,t.base,this.fallback);break;case"abstract":this.history=new jt(this,t.base)}}match(t,e,r){return this.matcher.match(t,e,r)}get currentRoute(){return this.history&&this.history.current}init(t){if(this.apps.push(t),t.$once("hook:destroyed",()=>{const e=this.apps.indexOf(t);e>-1&&this.apps.splice(e,1),this.app===t&&(this.app=this.apps[0]||null)}),this.app)return;this.app=t;const e=this.history;if(e instanceof Rt)e.transitionTo([e.getCurrentLocation()]);else if(e instanceof At){const t=()=>{e.setupListeners()};e.transitionTo(e.getCurrentLocation(),t,t)}e.listen((t,e)=>{this.apps.forEach(r=>{for(let n=e;nt.length;)r._routes.pop()})})}beforeEach(t){return St(this.beforeHooks,t)}beforeResolve(t){return St(this.resolveHooks,t)}afterEach(t){return St(this.afterHooks,t)}onReady(t,e){this.history.onReady(t,e)}onError(t){this.history.onError(t)}push(t,e,r){this.history.navigateLastLayer(t,!0,e,r)}replace(t,e,r){this.history.navigateLastLayer(t,!1,e,r)}pushAddLayer(t,e,r){this.history.navigateAddLayer(t,!0,e,r)}replaceAddLayer(t,e,r){this.history.navigateAddLayer(t,!1,e,r)}pushRemoveLayer(t,e){this.history.navigateRemoveLayer(!0,t,e)}replaceRemoveLayer(t,e){this.history.navigateRemoveLayer(!1,t,e)}pushLayer(t,e,r,n){this.history.navigateLayer(t,e,!0,r,n)}replaceLayer(t,e,r,n){this.history.navigateLayer(t,e,!1,r,n)}pushAllLayers(t,e,r){this.history.navigateAllLayers(t,!0,e,r)}replaceAllLayers(t,e,r){this.history.navigateAllLayers(t,!1,e,r)}go(t){this.history.go(t)}back(){this.go(-1)}forward(){this.go(1)}getMatchedComponents(t){const e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map(t=>Object.keys(t.components).map(e=>t.components[e]))):[]}resolve(t,e,r){const n=B(t,e=e||this.history.current,r,this),o=this.match(n,e),s=o.redirectedFrom||o.fullPath;return{location:n,route:o,href:function(t,e,r){var n="hash"===r?"#"+e:e;return t?b(t+"/"+n):n}(this.history.base,s,this.mode),normalizedTo:n,resolved:o}}addRoutes(t){this.matcher.addRoutes(t),this.history.current!==d&&this.history.transitionTo(this.history.getCurrentLocation())}}function St(t,e){return t.push(e),()=>{const r=t.indexOf(e);r>-1&&t.splice(r,1)}}Tt.install=function t(e){if(t.installed&&D===e)return;t.installed=!0,D=e;const r=t=>void 0!==t,o=(t,e)=>{let n=t.$options._parentVnode;r(n)&&r(n=n.data)&&r(n=n.registerRouteInstance)&&n(t,e)};e.mixin({beforeCreate(){r(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),this._routerLayer=0,e.util.defineReactive(this,"_routes",this._router.history.current)):(this._routerRoot=this.$parent&&this.$parent._routerRoot||this,this._routerLayer=this.$parent?this.$parent._routerLayer:0),o(this,this)},destroyed(){o(this)}}),Object.defineProperty(e.prototype,"$router",{get(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$routerLayer",{get(){return new K(this._routerRoot._router,this._routerLayer)}}),Object.defineProperty(e.prototype,"$route",{get(){return this._routerRoot._routes[this._routerLayer]}}),e.component("RouterView",n),e.component("RouterLink",z);const s=e.config.optionMergeStrategies;s.beforeRouteEnter=s.beforeRouteLeave=s.beforeRouteUpdate=s.created},Tt.version="3.0.7",N&&window.Vue&&window.Vue.use(Tt);export default Tt; \ No newline at end of file diff --git a/dist/vue-router.esm.js b/dist/vue-router.esm.js index 34e4483cb..07516d373 100644 --- a/dist/vue-router.esm.js +++ b/dist/vue-router.esm.js @@ -21,6 +21,10 @@ function isError (err) { return Object.prototype.toString.call(err).indexOf('Error') > -1 } +function isExtendedError (constructor, err) { + return err instanceof constructor || (err && err.name === constructor.name) +} + function extend (a, b) { for (var key in b) { a[key] = b[key]; @@ -35,6 +39,10 @@ var View = { name: { type: String, default: 'default' + }, + nextLayer: { + type: Boolean, + default: false } }, render: function render (_, ref) { @@ -50,9 +58,16 @@ var View = { // so that components rendered by router-view can resolve named slots var h = parent.$createElement; var name = props.name; - var route = parent.$route; var cache = parent._routerViewCache || (parent._routerViewCache = {}); + var layer = parent._routerLayer + (props.nextLayer ? 1 : 0); + // render empty node if we don't have this high layers + if (parent._routerRoot._routes.length <= layer) { + cache[name] = null; + return h() + } + var route = parent._routerRoot._routes[layer]; + // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. var depth = 0; @@ -96,6 +111,7 @@ var View = { ) { matched.instances[name] = val; } + vm._routerLayer = layer; } // also register instance in prepatch hook @@ -390,200 +406,6 @@ function queryIncludes (current, target) { /* */ -// work around weird flow bug -var toTypes = [String, Object]; -var eventTypes = [String, Array]; - -var Link = { - name: 'RouterLink', - props: { - to: { - type: toTypes, - required: true - }, - tag: { - type: String, - default: 'a' - }, - exact: Boolean, - append: Boolean, - replace: Boolean, - activeClass: String, - exactActiveClass: String, - event: { - type: eventTypes, - default: 'click' - } - }, - render: function render (h) { - var this$1 = this; - - var router = this.$router; - var current = this.$route; - var ref = router.resolve(this.to, current, this.append); - var location = ref.location; - var route = ref.route; - var href = ref.href; - - var classes = {}; - var globalActiveClass = router.options.linkActiveClass; - var globalExactActiveClass = router.options.linkExactActiveClass; - // Support global empty active class - var activeClassFallback = globalActiveClass == null - ? 'router-link-active' - : globalActiveClass; - var exactActiveClassFallback = globalExactActiveClass == null - ? 'router-link-exact-active' - : globalExactActiveClass; - var activeClass = this.activeClass == null - ? activeClassFallback - : this.activeClass; - var exactActiveClass = this.exactActiveClass == null - ? exactActiveClassFallback - : this.exactActiveClass; - var compareTarget = location.path - ? createRoute(null, location, null, router) - : route; - - classes[exactActiveClass] = isSameRoute(current, compareTarget); - classes[activeClass] = this.exact - ? classes[exactActiveClass] - : isIncludedRoute(current, compareTarget); - - var handler = function (e) { - if (guardEvent(e)) { - if (this$1.replace) { - router.replace(location); - } else { - router.push(location); - } - } - }; - - var on = { click: guardEvent }; - if (Array.isArray(this.event)) { - this.event.forEach(function (e) { on[e] = handler; }); - } else { - on[this.event] = handler; - } - - var data = { - class: classes - }; - - if (this.tag === 'a') { - data.on = on; - data.attrs = { href: href }; - } else { - // find the first child and apply listener and href - var a = findAnchor(this.$slots.default); - if (a) { - // in case the is a static node - a.isStatic = false; - var aData = a.data = extend({}, a.data); - aData.on = on; - var aAttrs = a.data.attrs = extend({}, a.data.attrs); - aAttrs.href = href; - } else { - // doesn't have child, apply listener to self - data.on = on; - } - } - - return h(this.tag, data, this.$slots.default) - } -} - -function guardEvent (e) { - // don't redirect with control keys - if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return } - // don't redirect when preventDefault called - if (e.defaultPrevented) { return } - // don't redirect on right click - if (e.button !== undefined && e.button !== 0) { return } - // don't redirect if `target="_blank"` - if (e.currentTarget && e.currentTarget.getAttribute) { - var target = e.currentTarget.getAttribute('target'); - if (/\b_blank\b/i.test(target)) { return } - } - // this may be a Weex event which doesn't have this method - if (e.preventDefault) { - e.preventDefault(); - } - return true -} - -function findAnchor (children) { - if (children) { - var child; - for (var i = 0; i < children.length; i++) { - child = children[i]; - if (child.tag === 'a') { - return child - } - if (child.children && (child = findAnchor(child.children))) { - return child - } - } - } -} - -var _Vue; - -function install (Vue) { - if (install.installed && _Vue === Vue) { return } - install.installed = true; - - _Vue = Vue; - - var isDef = function (v) { return v !== undefined; }; - - var registerInstance = function (vm, callVal) { - var i = vm.$options._parentVnode; - if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { - i(vm, callVal); - } - }; - - Vue.mixin({ - beforeCreate: function beforeCreate () { - if (isDef(this.$options.router)) { - this._routerRoot = this; - this._router = this.$options.router; - this._router.init(this); - Vue.util.defineReactive(this, '_route', this._router.history.current); - } else { - this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; - } - registerInstance(this, this); - }, - destroyed: function destroyed () { - registerInstance(this); - } - }); - - Object.defineProperty(Vue.prototype, '$router', { - get: function get () { return this._routerRoot._router } - }); - - Object.defineProperty(Vue.prototype, '$route', { - get: function get () { return this._routerRoot._route } - }); - - Vue.component('RouterView', View); - Vue.component('RouterLink', Link); - - var strats = Vue.config.optionMergeStrategies; - // use the same hook merging strategy for route hooks - strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; -} - -/* */ - -var inBrowser = typeof window !== 'undefined'; - -/* */ - function resolvePath ( relative, base, @@ -1048,77 +870,368 @@ function tokensToRegExp (tokens, keys, options) { route += strict && endsWithDelimiter ? '' : '(?=' + delimiter + '|$)'; } - return attachKeys(new RegExp('^' + route, flags(options)), keys) -} + return attachKeys(new RegExp('^' + route, flags(options)), keys) +} + +/** + * Normalize the given path string, returning a regular expression. + * + * An empty array can be passed in for the keys, which will hold the + * placeholder key descriptions. For example, using `/user/:id`, `keys` will + * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. + * + * @param {(string|RegExp|Array)} path + * @param {(Array|Object)=} keys + * @param {Object=} options + * @return {!RegExp} + */ +function pathToRegexp (path, keys, options) { + if (!isarray(keys)) { + options = /** @type {!Object} */ (keys || options); + keys = []; + } + + options = options || {}; + + if (path instanceof RegExp) { + return regexpToRegexp(path, /** @type {!Array} */ (keys)) + } + + if (isarray(path)) { + return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) + } + + return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) +} +pathToRegexp_1.parse = parse_1; +pathToRegexp_1.compile = compile_1; +pathToRegexp_1.tokensToFunction = tokensToFunction_1; +pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + +/* */ + +// $flow-disable-line +var regexpCompileCache = Object.create(null); + +function fillParams ( + path, + params, + routeMsg +) { + params = params || {}; + try { + var filler = + regexpCompileCache[path] || + (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + + // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} + if (params.pathMatch) { params[0] = params.pathMatch; } + + return filler(params, { pretty: true }) + } catch (e) { + if (process.env.NODE_ENV !== 'production') { + warn(false, ("missing param for " + routeMsg + ": " + (e.message))); + } + return '' + } finally { + // delete the 0 if it was added + delete params[0]; + } +} + +/* */ + +function normalizeLocation ( + raw, + current, + append, + router +) { + var next = typeof raw === 'string' ? { path: raw } : raw; + // named target + if (next._normalized) { + return next + } else if (next.name) { + return extend({}, raw) + } + + // relative params + if (!next.path && next.params && current) { + next = extend({}, next); + next._normalized = true; + var params = extend(extend({}, current.params), next.params); + if (current.name) { + next.name = current.name; + next.params = params; + } else if (current.matched.length) { + var rawPath = current.matched[current.matched.length - 1].path; + next.path = fillParams(rawPath, params, ("path " + (current.path))); + } else if (process.env.NODE_ENV !== 'production') { + warn(false, "relative params navigation requires a current route."); + } + return next + } + + var parsedPath = parsePath(next.path || ''); + var basePath = (current && current.path) || '/'; + var path = parsedPath.path + ? resolvePath(parsedPath.path, basePath, append || next.append) + : basePath; + + var query = resolveQuery( + parsedPath.query, + next.query, + router && router.options.parseQuery + ); + + var hash = next.hash || parsedPath.hash; + if (hash && hash.charAt(0) !== '#') { + hash = "#" + hash; + } + + return { + _normalized: true, + path: path, + query: query, + hash: hash + } +} + +/* */ + +// work around weird flow bug +var toTypes = [String, Object]; +var eventTypes = [String, Array]; + +var Link = { + name: 'RouterLink', + props: { + to: { + type: toTypes, + required: true + }, + tag: { + type: String, + default: 'a' + }, + exact: Boolean, + append: Boolean, + replace: Boolean, + addLayer: Boolean, + removeLayer: Boolean, + activeClass: String, + exactActiveClass: String, + event: { + type: eventTypes, + default: 'click' + } + }, + render: function render (h) { + var this$1 = this; + + var router = this.$router; + var current = this.$route; + var ref = router.resolve( + this.to, + current, + this.append + ); + var location = ref.location; + var route = ref.route; + var href = ref.href; + + var classes = {}; + var globalActiveClass = router.options.linkActiveClass; + var globalExactActiveClass = router.options.linkExactActiveClass; + // Support global empty active class + var activeClassFallback = + globalActiveClass == null ? 'router-link-active' : globalActiveClass; + var exactActiveClassFallback = + globalExactActiveClass == null + ? 'router-link-exact-active' + : globalExactActiveClass; + var activeClass = + this.activeClass == null ? activeClassFallback : this.activeClass; + var exactActiveClass = + this.exactActiveClass == null + ? exactActiveClassFallback + : this.exactActiveClass; + + var compareTarget = route.redirectedFrom + ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router) + : route; + + classes[exactActiveClass] = isSameRoute(current, compareTarget); + classes[activeClass] = this.exact + ? classes[exactActiveClass] + : isIncludedRoute(current, compareTarget); + + var handler = function (e) { + if (guardEvent(e)) { + if (this$1.replace) { + if (this$1.addLayer) { + router.replaceAddLayer(location); + } else if (this$1.removeLayer) { + router.replaceRemoveLayer(); + } else { + router.replaceLayer(this$1._routerLayer, location); + } + } else { + if (this$1.addLayer) { + router.pushAddLayer(location); + } else if (this$1.removeLayer) { + router.pushRemoveLayer(); + } else { + router.pushLayer(this$1._routerLayer, location); + } + } + } + }; + + var on = { click: guardEvent }; + if (Array.isArray(this.event)) { + this.event.forEach(function (e) { + on[e] = handler; + }); + } else { + on[this.event] = handler; + } + + var data = { + class: classes + }; + + if (this.tag === 'a') { + data.on = on; + data.attrs = { href: href }; + } else { + // find the first child and apply listener and href + var a = findAnchor(this.$slots.default); + if (a) { + // in case the is a static node + a.isStatic = false; + var aData = (a.data = extend({}, a.data)); + aData.on = on; + var aAttrs = (a.data.attrs = extend({}, a.data.attrs)); + aAttrs.href = href; + } else { + // doesn't have child, apply listener to self + data.on = on; + } + } + + return h(this.tag, data, this.$slots.default) + } +} + +function guardEvent (e) { + // don't redirect with control keys + if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return } + // don't redirect when preventDefault called + if (e.defaultPrevented) { return } + // don't redirect on right click + if (e.button !== undefined && e.button !== 0) { return } + // don't redirect if `target="_blank"` + if (e.currentTarget && e.currentTarget.getAttribute) { + var target = e.currentTarget.getAttribute('target'); + if (/\b_blank\b/i.test(target)) { return } + } + // this may be a Weex event which doesn't have this method + if (e.preventDefault) { + e.preventDefault(); + } + return true +} + +function findAnchor (children) { + if (children) { + var child; + for (var i = 0; i < children.length; i++) { + child = children[i]; + if (child.tag === 'a') { + return child + } + if (child.children && (child = findAnchor(child.children))) { + return child + } + } + } +} + +var _Vue; +var VueRouterLayer = function VueRouterLayer (router, layer) { + this.router = router; + this.layer = layer; +}; + +VueRouterLayer.prototype.push = function push (location, onComplete, onAbort) { + return this.router.pushLayer(this.layer, location, onComplete, onAbort) +}; + +VueRouterLayer.prototype.replace = function replace (location, onComplete, onAbort) { + return this.router.replaceLayer(this.layer, location, onComplete, onAbort) +}; -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - * - * @param {(string|RegExp|Array)} path - * @param {(Array|Object)=} keys - * @param {Object=} options - * @return {!RegExp} - */ -function pathToRegexp (path, keys, options) { - if (!isarray(keys)) { - options = /** @type {!Object} */ (keys || options); - keys = []; - } +function install (Vue) { + if (install.installed && _Vue === Vue) { return } + install.installed = true; - options = options || {}; + _Vue = Vue; - if (path instanceof RegExp) { - return regexpToRegexp(path, /** @type {!Array} */ (keys)) - } + var isDef = function (v) { return v !== undefined; }; - if (isarray(path)) { - return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) - } + var registerInstance = function (vm, callVal) { + var i = vm.$options._parentVnode; + if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { + i(vm, callVal); + } + }; - return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) -} -pathToRegexp_1.parse = parse_1; -pathToRegexp_1.compile = compile_1; -pathToRegexp_1.tokensToFunction = tokensToFunction_1; -pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + Vue.mixin({ + beforeCreate: function beforeCreate () { + if (isDef(this.$options.router)) { + this._routerRoot = this; + this._router = this.$options.router; + this._router.init(this); + this._routerLayer = 0; + Vue.util.defineReactive(this, '_routes', this._router.history.current); + } else { + this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; + this._routerLayer = this.$parent ? this.$parent._routerLayer : 0; + } + registerInstance(this, this); + }, + destroyed: function destroyed () { + registerInstance(this); + } + }); -/* */ + Object.defineProperty(Vue.prototype, '$router', { + get: function get () { return this._routerRoot._router } + }); -// $flow-disable-line -var regexpCompileCache = Object.create(null); + Object.defineProperty(Vue.prototype, '$routerLayer', { + get: function get () { return new VueRouterLayer(this._routerRoot._router, this._routerLayer) } + }); -function fillParams ( - path, - params, - routeMsg -) { - params = params || {}; - try { - var filler = - regexpCompileCache[path] || - (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + Object.defineProperty(Vue.prototype, '$route', { + get: function get () { return this._routerRoot._routes[this._routerLayer] } + }); - // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} - if (params.pathMatch) { params[0] = params.pathMatch; } + Vue.component('RouterView', View); + Vue.component('RouterLink', Link); - return filler(params, { pretty: true }) - } catch (e) { - if (process.env.NODE_ENV !== 'production') { - warn(false, ("missing param for " + routeMsg + ": " + (e.message))); - } - return '' - } finally { - // delete the 0 if it was added - delete params[0]; - } + var strats = Vue.config.optionMergeStrategies; + // use the same hook merging strategy for route hooks + strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; } /* */ +var inBrowser = typeof window !== 'undefined'; + +/* */ + function createRouteMap ( routes, oldPathList, @@ -1284,64 +1397,6 @@ function normalizePath (path, parent, strict) { /* */ -function normalizeLocation ( - raw, - current, - append, - router -) { - var next = typeof raw === 'string' ? { path: raw } : raw; - // named target - if (next._normalized) { - return next - } else if (next.name) { - return extend({}, raw) - } - - // relative params - if (!next.path && next.params && current) { - next = extend({}, next); - next._normalized = true; - var params = extend(extend({}, current.params), next.params); - if (current.name) { - next.name = current.name; - next.params = params; - } else if (current.matched.length) { - var rawPath = current.matched[current.matched.length - 1].path; - next.path = fillParams(rawPath, params, ("path " + (current.path))); - } else if (process.env.NODE_ENV !== 'production') { - warn(false, "relative params navigation requires a current route."); - } - return next - } - - var parsedPath = parsePath(next.path || ''); - var basePath = (current && current.path) || '/'; - var path = parsedPath.path - ? resolvePath(parsedPath.path, basePath, append || next.append) - : basePath; - - var query = resolveQuery( - parsedPath.query, - next.query, - router && router.options.parseQuery - ); - - var hash = next.hash || parsedPath.hash; - if (hash && hash.charAt(0) !== '#') { - hash = "#" + hash; - } - - return { - _normalized: true, - path: path, - query: query, - hash: hash - } -} - -/* */ - function createMatcher ( @@ -1579,20 +1634,27 @@ function handleScroll ( // wait until re-render finishes before scrolling router.app.$nextTick(function () { var position = getScrollPosition(); - var shouldScroll = behavior.call(router, to, from, isPop ? position : null); + var shouldScroll = behavior.call( + router, + to, + from, + isPop ? position : null + ); if (!shouldScroll) { return } if (typeof shouldScroll.then === 'function') { - shouldScroll.then(function (shouldScroll) { - scrollToPosition((shouldScroll), position); - }).catch(function (err) { - if (process.env.NODE_ENV !== 'production') { - assert(false, err.toString()); - } - }); + shouldScroll + .then(function (shouldScroll) { + scrollToPosition((shouldScroll), position); + }) + .catch(function (err) { + if (process.env.NODE_ENV !== 'production') { + assert(false, err.toString()); + } + }); } else { scrollToPosition(shouldScroll, position); } @@ -1648,12 +1710,22 @@ function isNumber (v) { return typeof v === 'number' } +var hashStartsWithNumberRE = /^#\d/; + function scrollToPosition (shouldScroll, position) { var isObject = typeof shouldScroll === 'object'; if (isObject && typeof shouldScroll.selector === 'string') { - var el = document.querySelector(shouldScroll.selector); + // getElementById would still fail if the selector contains a more complicated query like #main[data-attr] + // but at the same time, it doesn't make much sense to select an element with an id and an extra selector + var el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line + ? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line + : document.querySelector(shouldScroll.selector); + if (el) { - var offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {}; + var offset = + shouldScroll.offset && typeof shouldScroll.offset === 'object' + ? shouldScroll.offset + : {}; offset = normalizeOffset(offset); position = getElementPosition(el, offset); } else if (isValidPosition(shouldScroll)) { @@ -1704,17 +1776,17 @@ function setStateKey (key) { _key = key; } -function pushState (url, replace) { +function pushState (url, state, replace) { saveScrollPosition(); // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls var history = window.history; try { if (replace) { - history.replaceState({ key: _key }, '', url); + history.replaceState({ key: _key, state: state }, '', url); } else { _key = genKey(); - history.pushState({ key: _key }, '', url); + history.pushState({ key: _key, state: state }, '', url); } } catch (e) { window.location[replace ? 'replace' : 'assign'](url); @@ -1853,13 +1925,26 @@ function once (fn) { } } +var NavigationDuplicated = /*@__PURE__*/(function (Error) { + function NavigationDuplicated () { + Error.call(this, 'Navigating to current location is not allowed'); + this.name = 'NavigationDuplicated'; + } + + if ( Error ) NavigationDuplicated.__proto__ = Error; + NavigationDuplicated.prototype = Object.create( Error && Error.prototype ); + NavigationDuplicated.prototype.constructor = NavigationDuplicated; + + return NavigationDuplicated; +}(Error)); + /* */ var History = function History (router, base) { this.router = router; this.base = normalizeBase(base); // start with a route object that stands for "nowhere" - this.current = START; + this.current = [START]; this.pending = null; this.ready = false; this.readyCbs = []; @@ -1886,39 +1971,64 @@ History.prototype.onError = function onError (errorCb) { this.errorCbs.push(errorCb); }; -History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) { - var this$1 = this; +History.prototype.getClosestCurrent = function getClosestCurrent (n) { + if (n >= this.current.length) { + return this.current[this.current.length - 1] + } + return this.current[n] +}; - var route = this.router.match(location, this.current); - this.confirmTransition(route, function () { - this$1.updateRoute(route); - onComplete && onComplete(route); - this$1.ensureURL(); +History.prototype.transitionTo = function transitionTo ( + locations, + onComplete, + onAbort +) { + var this$1 = this; - // fire ready cbs once - if (!this$1.ready) { - this$1.ready = true; - this$1.readyCbs.forEach(function (cb) { cb(route); }); - } - }, function (err) { - if (onAbort) { - onAbort(err); - } - if (err && !this$1.ready) { - this$1.ready = true; - this$1.readyErrorCbs.forEach(function (cb) { cb(err); }); + var routes = locations.map(function (location, i) { return this$1.router.match(location, this$1.getClosestCurrent(i)); }); + this.confirmTransition( + routes, + function (equalLayers) { + this$1.updateCurrent(routes, equalLayers); + onComplete && onComplete(routes); + this$1.ensureURL(); + + // fire ready cbs once + if (!this$1.ready) { + this$1.ready = true; + this$1.readyCbs.forEach(function (cb) { + cb(routes); + }); + } + }, + function (err) { + if (onAbort) { + onAbort(err); + } + if (err && !this$1.ready) { + this$1.ready = true; + this$1.readyErrorCbs.forEach(function (cb) { + cb(err); + }); + } } - }); + ); }; -History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { +History.prototype.confirmTransition = function confirmTransition (routes, onComplete, onAbort) { var this$1 = this; var current = this.current; var abort = function (err) { - if (isError(err)) { + // after merging https://github.com/vuejs/vue-router/pull/2771 we + // When the user navigates through history through back/forward buttons + // we do not want to throw the error. We only throw it if directly calling + // push/replace. That's why it's not included in isError + if (!isExtendedError(NavigationDuplicated, err) && isError(err)) { if (this$1.errorCbs.length) { - this$1.errorCbs.forEach(function (cb) { cb(err); }); + this$1.errorCbs.forEach(function (cb) { + cb(err); + }); } else { warn(false, 'uncaught error during route navigation:'); console.error(err); @@ -1926,16 +2036,28 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl } onAbort && onAbort(err); }; - if ( - isSameRoute(route, current) && - // in the case the route map has been dynamically appended to - route.matched.length === current.matched.length - ) { + + var equalLayers = 0; + for (equalLayers = 0; equalLayers < routes.length && equalLayers < current.length; equalLayers++) { + if ( + !isSameRoute(routes[equalLayers], current[equalLayers]) || + // in the case the route map has been dynamically appended to + routes[equalLayers].matched.length !== current[equalLayers].matched.length + ) { + break + } + } + + if (equalLayers === routes.length && equalLayers === current.length) { this.ensureURL(); - return abort() + return abort(new NavigationDuplicated(routes)) } - var ref = resolveQueue(this.current.matched, route.matched); + var ref = resolveQueues( + current, + routes, + equalLayers + ); var updated = ref.updated; var deactivated = ref.deactivated; var activated = ref.activated; @@ -1953,23 +2075,21 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl resolveAsyncComponents(activated) ); - this.pending = route; + this.pending = routes; var iterator = function (hook, next) { - if (this$1.pending !== route) { + if (this$1.pending !== routes) { return abort() } try { - hook(route, current, function (to) { + hook(routes, current, function (to) { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this$1.ensureURL(true); abort(to); } else if ( typeof to === 'string' || - (typeof to === 'object' && ( - typeof to.path === 'string' || - typeof to.name === 'string' - )) + (typeof to === 'object' && + (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({ path: '/' }) -> redirect abort(); @@ -1990,32 +2110,34 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl runQueue(queue, iterator, function () { var postEnterCbs = []; - var isValid = function () { return this$1.current === route; }; + var isValid = function () { return this$1.current === routes; }; // wait until async components are resolved before // extracting in-component enter guards var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); var queue = enterGuards.concat(this$1.router.resolveHooks); runQueue(queue, iterator, function () { - if (this$1.pending !== route) { + if (this$1.pending !== routes) { return abort() } this$1.pending = null; - onComplete(route); + onComplete(equalLayers); if (this$1.router.app) { this$1.router.app.$nextTick(function () { - postEnterCbs.forEach(function (cb) { cb(); }); + postEnterCbs.forEach(function (cb) { + cb(); + }); }); } }); }); }; -History.prototype.updateRoute = function updateRoute (route) { +History.prototype.updateCurrent = function updateCurrent (routes, equalLayers) { var prev = this.current; - this.current = route; - this.cb && this.cb(route); + this.current = routes; + this.cb && this.cb(routes, equalLayers); this.router.afterHooks.forEach(function (hook) { - hook && hook(route, prev); + hook && hook(routes, prev); }); }; @@ -2039,6 +2161,35 @@ function normalizeBase (base) { return base.replace(/\/$/, '') } +function resolveQueues ( + current, + next, + equalLayers +) { + var ref, ref$1, ref$2, ref$3, ref$4; + + var res = { + updated: [], + activated: [], + deactivated: [] + }; + + var min = Math.min(current.length, next.length); + for (var i = equalLayers; i < min; i++) { + var r = resolveQueue(current[i].matched, next[i].matched) + (ref = res.updated).push.apply(ref, r.updated) + (ref$1 = res.activated).push.apply(ref$1, r.activated) + (ref$2 = res.deactivated).push.apply(ref$2, r.deactivated); + } + for (var i$1 = min; i$1 < current.length; i$1++) { + (ref$3 = res.deactivated).push.apply(ref$3, current[i$1].matched); + } + for (var i$2 = min; i$2 < next.length; i$2++) { + (ref$4 = res.activated).push.apply(ref$4, next[i$2].matched); + } + return res +} + function resolveQueue ( current, next @@ -2106,9 +2257,13 @@ function extractEnterGuards ( cbs, isValid ) { - return extractGuards(activated, 'beforeRouteEnter', function (guard, _, match, key) { - return bindEnterGuard(guard, match, key, cbs, isValid) - }) + return extractGuards( + activated, + 'beforeRouteEnter', + function (guard, _, match, key) { + return bindEnterGuard(guard, match, key, cbs, isValid) + } + ) } function bindEnterGuard ( @@ -2179,7 +2334,11 @@ var HTML5History = /*@__PURE__*/(function (History$$1) { return } - this$1.transitionTo(location, function (route) { + var locations = [location]; + if (window.history.state.state) { + locations = window.history.state.state; + } + this$1.transitionTo(locations, function (route) { if (supportsScroll) { handleScroll(router, route, current, true); } @@ -2195,34 +2354,48 @@ var HTML5History = /*@__PURE__*/(function (History$$1) { window.history.go(n); }; - HTML5History.prototype.push = function push (location, onComplete, onAbort) { + HTML5History.prototype.navigateAllLayers = function navigateAllLayers (locations, push, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; - this.transitionTo(location, function (route) { - pushState(cleanPath(this$1.base + route.fullPath)); + this.transitionTo(locations, function (routes) { + this$1.ensureURL(push); + var route = this$1.current[this$1.current.length - 1]; handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }; - HTML5History.prototype.replace = function replace (location, onComplete, onAbort) { - var this$1 = this; + HTML5History.prototype.navigateLastLayer = function navigateLastLayer (location, push, onComplete, onAbort) { + var locations = this.current.slice(0, -1).map(function (r) { return r.fullPath; }).concat( [location] + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; - var ref = this; - var fromRoute = ref.current; - this.transitionTo(location, function (route) { - replaceState(cleanPath(this$1.base + route.fullPath)); - handleScroll(this$1.router, route, fromRoute, false); - onComplete && onComplete(route); - }, onAbort); + HTML5History.prototype.navigateLayer = function navigateLayer (layer, location, push, onComplete, onAbort) { + var locations = this.current.slice(0, layer).map(function (r) { return r.fullPath; }).concat( [location], + this.current.slice(layer + 1).map(function (r) { return r.fullPath; }) + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; + + HTML5History.prototype.navigateAddLayer = function navigateAddLayer (location, push, onComplete, onAbort) { + var locations = this.current.map(function (r) { return r.fullPath; }).concat( [location] + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; + + HTML5History.prototype.navigateRemoveLayer = function navigateRemoveLayer (location, push, onComplete, onAbort) { + var locations = this.current.slice(0, -1).map(function (r) { return r.fullPath; }); + this.navigateAllLayers(locations, push, onComplete, onAbort); }; HTML5History.prototype.ensureURL = function ensureURL (push) { - if (getLocation(this.base) !== this.current.fullPath) { - var current = cleanPath(this.base + this.current.fullPath); - push ? pushState(current) : replaceState(current); + var route = this.current[this.current.length - 1]; + if (getLocation(this.base) !== route.fullPath) { + var path = cleanPath(this.base + route.fullPath); + pushState(path, this.current.map(function (r) { return r.fullPath; }), !push); } }; @@ -2410,20 +2583,28 @@ var AbstractHistory = /*@__PURE__*/(function (History$$1) { AbstractHistory.prototype.push = function push (location, onComplete, onAbort) { var this$1 = this; - this.transitionTo(location, function (route) { - this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route); - this$1.index++; - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + function (route) { + this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route); + this$1.index++; + onComplete && onComplete(route); + }, + onAbort + ); }; AbstractHistory.prototype.replace = function replace (location, onComplete, onAbort) { var this$1 = this; - this.transitionTo(location, function (route) { - this$1.stack = this$1.stack.slice(0, this$1.index).concat(route); - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + function (route) { + this$1.stack = this$1.stack.slice(0, this$1.index).concat(route); + onComplete && onComplete(route); + }, + onAbort + ); }; AbstractHistory.prototype.go = function go (n) { @@ -2434,10 +2615,18 @@ var AbstractHistory = /*@__PURE__*/(function (History$$1) { return } var route = this.stack[targetIndex]; - this.confirmTransition(route, function () { - this$1.index = targetIndex; - this$1.updateRoute(route); - }); + this.confirmTransition( + route, + function () { + this$1.index = targetIndex; + this$1.updateRoute(route); + }, + function (err) { + if (isExtendedError(NavigationDuplicated, err)) { + this$1.index = targetIndex; + } + } + ); }; AbstractHistory.prototype.getCurrentLocation = function getCurrentLocation () { @@ -2541,7 +2730,7 @@ VueRouter.prototype.init = function init (app /* Vue component instance */) { var history = this.history; if (history instanceof HTML5History) { - history.transitionTo(history.getCurrentLocation()); + history.transitionTo([history.getCurrentLocation()]); } else if (history instanceof HashHistory) { var setupHashListener = function () { history.setupListeners(); @@ -2553,9 +2742,21 @@ VueRouter.prototype.init = function init (app /* Vue component instance */) { ); } - history.listen(function (route) { + history.listen(function (routes, equalLayers) { this$1.apps.forEach(function (app) { - app._route = route; + // Mutate only the changed routes, so we don't trigger unnecessary updates. + // Changed elements + for (var i = equalLayers; i < app._routes.length && i < routes.length; i++) { + app.$set(app._routes, i, routes[i]); + } + // Added elements + for (var i$1 = app._routes.length; i$1 < routes.length; i$1++) { + app._routes.push(routes[i$1]); + } + // Removed + while (app._routes.length > routes.length) { + app._routes.pop(); + } }); }); }; @@ -2581,11 +2782,43 @@ VueRouter.prototype.onError = function onError (errorCb) { }; VueRouter.prototype.push = function push (location, onComplete, onAbort) { - this.history.push(location, onComplete, onAbort); + this.history.navigateLastLayer(location, true, onComplete, onAbort); }; VueRouter.prototype.replace = function replace (location, onComplete, onAbort) { - this.history.replace(location, onComplete, onAbort); + this.history.navigateLastLayer(location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushAddLayer = function pushAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceAddLayer = function replaceAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushRemoveLayer = function pushRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceRemoveLayer = function replaceRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(false, onComplete, onAbort); +}; + +VueRouter.prototype.pushLayer = function pushLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceLayer = function replaceLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushAllLayers = function pushAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceAllLayers = function replaceAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, false, onComplete, onAbort); }; VueRouter.prototype.go = function go (n) { diff --git a/dist/vue-router.js b/dist/vue-router.js index f8ac657d5..f0b4aa1e4 100644 --- a/dist/vue-router.js +++ b/dist/vue-router.js @@ -27,6 +27,10 @@ function isError (err) { return Object.prototype.toString.call(err).indexOf('Error') > -1 } +function isExtendedError (constructor, err) { + return err instanceof constructor || (err && err.name === constructor.name) +} + function extend (a, b) { for (var key in b) { a[key] = b[key]; @@ -41,6 +45,10 @@ var View = { name: { type: String, default: 'default' + }, + nextLayer: { + type: Boolean, + default: false } }, render: function render (_, ref) { @@ -56,9 +64,16 @@ var View = { // so that components rendered by router-view can resolve named slots var h = parent.$createElement; var name = props.name; - var route = parent.$route; var cache = parent._routerViewCache || (parent._routerViewCache = {}); + var layer = parent._routerLayer + (props.nextLayer ? 1 : 0); + // render empty node if we don't have this high layers + if (parent._routerRoot._routes.length <= layer) { + cache[name] = null; + return h() + } + var route = parent._routerRoot._routes[layer]; + // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. var depth = 0; @@ -102,6 +117,7 @@ var View = { ) { matched.instances[name] = val; } + vm._routerLayer = layer; } // also register instance in prepatch hook @@ -396,200 +412,6 @@ function queryIncludes (current, target) { /* */ -// work around weird flow bug -var toTypes = [String, Object]; -var eventTypes = [String, Array]; - -var Link = { - name: 'RouterLink', - props: { - to: { - type: toTypes, - required: true - }, - tag: { - type: String, - default: 'a' - }, - exact: Boolean, - append: Boolean, - replace: Boolean, - activeClass: String, - exactActiveClass: String, - event: { - type: eventTypes, - default: 'click' - } - }, - render: function render (h) { - var this$1 = this; - - var router = this.$router; - var current = this.$route; - var ref = router.resolve(this.to, current, this.append); - var location = ref.location; - var route = ref.route; - var href = ref.href; - - var classes = {}; - var globalActiveClass = router.options.linkActiveClass; - var globalExactActiveClass = router.options.linkExactActiveClass; - // Support global empty active class - var activeClassFallback = globalActiveClass == null - ? 'router-link-active' - : globalActiveClass; - var exactActiveClassFallback = globalExactActiveClass == null - ? 'router-link-exact-active' - : globalExactActiveClass; - var activeClass = this.activeClass == null - ? activeClassFallback - : this.activeClass; - var exactActiveClass = this.exactActiveClass == null - ? exactActiveClassFallback - : this.exactActiveClass; - var compareTarget = location.path - ? createRoute(null, location, null, router) - : route; - - classes[exactActiveClass] = isSameRoute(current, compareTarget); - classes[activeClass] = this.exact - ? classes[exactActiveClass] - : isIncludedRoute(current, compareTarget); - - var handler = function (e) { - if (guardEvent(e)) { - if (this$1.replace) { - router.replace(location); - } else { - router.push(location); - } - } - }; - - var on = { click: guardEvent }; - if (Array.isArray(this.event)) { - this.event.forEach(function (e) { on[e] = handler; }); - } else { - on[this.event] = handler; - } - - var data = { - class: classes - }; - - if (this.tag === 'a') { - data.on = on; - data.attrs = { href: href }; - } else { - // find the first child and apply listener and href - var a = findAnchor(this.$slots.default); - if (a) { - // in case the is a static node - a.isStatic = false; - var aData = a.data = extend({}, a.data); - aData.on = on; - var aAttrs = a.data.attrs = extend({}, a.data.attrs); - aAttrs.href = href; - } else { - // doesn't have child, apply listener to self - data.on = on; - } - } - - return h(this.tag, data, this.$slots.default) - } -} - -function guardEvent (e) { - // don't redirect with control keys - if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return } - // don't redirect when preventDefault called - if (e.defaultPrevented) { return } - // don't redirect on right click - if (e.button !== undefined && e.button !== 0) { return } - // don't redirect if `target="_blank"` - if (e.currentTarget && e.currentTarget.getAttribute) { - var target = e.currentTarget.getAttribute('target'); - if (/\b_blank\b/i.test(target)) { return } - } - // this may be a Weex event which doesn't have this method - if (e.preventDefault) { - e.preventDefault(); - } - return true -} - -function findAnchor (children) { - if (children) { - var child; - for (var i = 0; i < children.length; i++) { - child = children[i]; - if (child.tag === 'a') { - return child - } - if (child.children && (child = findAnchor(child.children))) { - return child - } - } - } -} - -var _Vue; - -function install (Vue) { - if (install.installed && _Vue === Vue) { return } - install.installed = true; - - _Vue = Vue; - - var isDef = function (v) { return v !== undefined; }; - - var registerInstance = function (vm, callVal) { - var i = vm.$options._parentVnode; - if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { - i(vm, callVal); - } - }; - - Vue.mixin({ - beforeCreate: function beforeCreate () { - if (isDef(this.$options.router)) { - this._routerRoot = this; - this._router = this.$options.router; - this._router.init(this); - Vue.util.defineReactive(this, '_route', this._router.history.current); - } else { - this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; - } - registerInstance(this, this); - }, - destroyed: function destroyed () { - registerInstance(this); - } - }); - - Object.defineProperty(Vue.prototype, '$router', { - get: function get () { return this._routerRoot._router } - }); - - Object.defineProperty(Vue.prototype, '$route', { - get: function get () { return this._routerRoot._route } - }); - - Vue.component('RouterView', View); - Vue.component('RouterLink', Link); - - var strats = Vue.config.optionMergeStrategies; - // use the same hook merging strategy for route hooks - strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; -} - -/* */ - -var inBrowser = typeof window !== 'undefined'; - -/* */ - function resolvePath ( relative, base, @@ -1054,77 +876,368 @@ function tokensToRegExp (tokens, keys, options) { route += strict && endsWithDelimiter ? '' : '(?=' + delimiter + '|$)'; } - return attachKeys(new RegExp('^' + route, flags(options)), keys) -} + return attachKeys(new RegExp('^' + route, flags(options)), keys) +} + +/** + * Normalize the given path string, returning a regular expression. + * + * An empty array can be passed in for the keys, which will hold the + * placeholder key descriptions. For example, using `/user/:id`, `keys` will + * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. + * + * @param {(string|RegExp|Array)} path + * @param {(Array|Object)=} keys + * @param {Object=} options + * @return {!RegExp} + */ +function pathToRegexp (path, keys, options) { + if (!isarray(keys)) { + options = /** @type {!Object} */ (keys || options); + keys = []; + } + + options = options || {}; + + if (path instanceof RegExp) { + return regexpToRegexp(path, /** @type {!Array} */ (keys)) + } + + if (isarray(path)) { + return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) + } + + return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) +} +pathToRegexp_1.parse = parse_1; +pathToRegexp_1.compile = compile_1; +pathToRegexp_1.tokensToFunction = tokensToFunction_1; +pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + +/* */ + +// $flow-disable-line +var regexpCompileCache = Object.create(null); + +function fillParams ( + path, + params, + routeMsg +) { + params = params || {}; + try { + var filler = + regexpCompileCache[path] || + (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + + // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} + if (params.pathMatch) { params[0] = params.pathMatch; } + + return filler(params, { pretty: true }) + } catch (e) { + { + warn(false, ("missing param for " + routeMsg + ": " + (e.message))); + } + return '' + } finally { + // delete the 0 if it was added + delete params[0]; + } +} + +/* */ + +function normalizeLocation ( + raw, + current, + append, + router +) { + var next = typeof raw === 'string' ? { path: raw } : raw; + // named target + if (next._normalized) { + return next + } else if (next.name) { + return extend({}, raw) + } + + // relative params + if (!next.path && next.params && current) { + next = extend({}, next); + next._normalized = true; + var params = extend(extend({}, current.params), next.params); + if (current.name) { + next.name = current.name; + next.params = params; + } else if (current.matched.length) { + var rawPath = current.matched[current.matched.length - 1].path; + next.path = fillParams(rawPath, params, ("path " + (current.path))); + } else { + warn(false, "relative params navigation requires a current route."); + } + return next + } + + var parsedPath = parsePath(next.path || ''); + var basePath = (current && current.path) || '/'; + var path = parsedPath.path + ? resolvePath(parsedPath.path, basePath, append || next.append) + : basePath; + + var query = resolveQuery( + parsedPath.query, + next.query, + router && router.options.parseQuery + ); + + var hash = next.hash || parsedPath.hash; + if (hash && hash.charAt(0) !== '#') { + hash = "#" + hash; + } + + return { + _normalized: true, + path: path, + query: query, + hash: hash + } +} + +/* */ + +// work around weird flow bug +var toTypes = [String, Object]; +var eventTypes = [String, Array]; + +var Link = { + name: 'RouterLink', + props: { + to: { + type: toTypes, + required: true + }, + tag: { + type: String, + default: 'a' + }, + exact: Boolean, + append: Boolean, + replace: Boolean, + addLayer: Boolean, + removeLayer: Boolean, + activeClass: String, + exactActiveClass: String, + event: { + type: eventTypes, + default: 'click' + } + }, + render: function render (h) { + var this$1 = this; + + var router = this.$router; + var current = this.$route; + var ref = router.resolve( + this.to, + current, + this.append + ); + var location = ref.location; + var route = ref.route; + var href = ref.href; + + var classes = {}; + var globalActiveClass = router.options.linkActiveClass; + var globalExactActiveClass = router.options.linkExactActiveClass; + // Support global empty active class + var activeClassFallback = + globalActiveClass == null ? 'router-link-active' : globalActiveClass; + var exactActiveClassFallback = + globalExactActiveClass == null + ? 'router-link-exact-active' + : globalExactActiveClass; + var activeClass = + this.activeClass == null ? activeClassFallback : this.activeClass; + var exactActiveClass = + this.exactActiveClass == null + ? exactActiveClassFallback + : this.exactActiveClass; + + var compareTarget = route.redirectedFrom + ? createRoute(null, normalizeLocation(route.redirectedFrom), null, router) + : route; + + classes[exactActiveClass] = isSameRoute(current, compareTarget); + classes[activeClass] = this.exact + ? classes[exactActiveClass] + : isIncludedRoute(current, compareTarget); + + var handler = function (e) { + if (guardEvent(e)) { + if (this$1.replace) { + if (this$1.addLayer) { + router.replaceAddLayer(location); + } else if (this$1.removeLayer) { + router.replaceRemoveLayer(); + } else { + router.replaceLayer(this$1._routerLayer, location); + } + } else { + if (this$1.addLayer) { + router.pushAddLayer(location); + } else if (this$1.removeLayer) { + router.pushRemoveLayer(); + } else { + router.pushLayer(this$1._routerLayer, location); + } + } + } + }; + + var on = { click: guardEvent }; + if (Array.isArray(this.event)) { + this.event.forEach(function (e) { + on[e] = handler; + }); + } else { + on[this.event] = handler; + } + + var data = { + class: classes + }; + + if (this.tag === 'a') { + data.on = on; + data.attrs = { href: href }; + } else { + // find the first child and apply listener and href + var a = findAnchor(this.$slots.default); + if (a) { + // in case the is a static node + a.isStatic = false; + var aData = (a.data = extend({}, a.data)); + aData.on = on; + var aAttrs = (a.data.attrs = extend({}, a.data.attrs)); + aAttrs.href = href; + } else { + // doesn't have child, apply listener to self + data.on = on; + } + } + + return h(this.tag, data, this.$slots.default) + } +} + +function guardEvent (e) { + // don't redirect with control keys + if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) { return } + // don't redirect when preventDefault called + if (e.defaultPrevented) { return } + // don't redirect on right click + if (e.button !== undefined && e.button !== 0) { return } + // don't redirect if `target="_blank"` + if (e.currentTarget && e.currentTarget.getAttribute) { + var target = e.currentTarget.getAttribute('target'); + if (/\b_blank\b/i.test(target)) { return } + } + // this may be a Weex event which doesn't have this method + if (e.preventDefault) { + e.preventDefault(); + } + return true +} + +function findAnchor (children) { + if (children) { + var child; + for (var i = 0; i < children.length; i++) { + child = children[i]; + if (child.tag === 'a') { + return child + } + if (child.children && (child = findAnchor(child.children))) { + return child + } + } + } +} + +var _Vue; +var VueRouterLayer = function VueRouterLayer (router, layer) { + this.router = router; + this.layer = layer; +}; + +VueRouterLayer.prototype.push = function push (location, onComplete, onAbort) { + return this.router.pushLayer(this.layer, location, onComplete, onAbort) +}; + +VueRouterLayer.prototype.replace = function replace (location, onComplete, onAbort) { + return this.router.replaceLayer(this.layer, location, onComplete, onAbort) +}; -/** - * Normalize the given path string, returning a regular expression. - * - * An empty array can be passed in for the keys, which will hold the - * placeholder key descriptions. For example, using `/user/:id`, `keys` will - * contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`. - * - * @param {(string|RegExp|Array)} path - * @param {(Array|Object)=} keys - * @param {Object=} options - * @return {!RegExp} - */ -function pathToRegexp (path, keys, options) { - if (!isarray(keys)) { - options = /** @type {!Object} */ (keys || options); - keys = []; - } +function install (Vue) { + if (install.installed && _Vue === Vue) { return } + install.installed = true; - options = options || {}; + _Vue = Vue; - if (path instanceof RegExp) { - return regexpToRegexp(path, /** @type {!Array} */ (keys)) - } + var isDef = function (v) { return v !== undefined; }; - if (isarray(path)) { - return arrayToRegexp(/** @type {!Array} */ (path), /** @type {!Array} */ (keys), options) - } + var registerInstance = function (vm, callVal) { + var i = vm.$options._parentVnode; + if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { + i(vm, callVal); + } + }; - return stringToRegexp(/** @type {string} */ (path), /** @type {!Array} */ (keys), options) -} -pathToRegexp_1.parse = parse_1; -pathToRegexp_1.compile = compile_1; -pathToRegexp_1.tokensToFunction = tokensToFunction_1; -pathToRegexp_1.tokensToRegExp = tokensToRegExp_1; + Vue.mixin({ + beforeCreate: function beforeCreate () { + if (isDef(this.$options.router)) { + this._routerRoot = this; + this._router = this.$options.router; + this._router.init(this); + this._routerLayer = 0; + Vue.util.defineReactive(this, '_routes', this._router.history.current); + } else { + this._routerRoot = (this.$parent && this.$parent._routerRoot) || this; + this._routerLayer = this.$parent ? this.$parent._routerLayer : 0; + } + registerInstance(this, this); + }, + destroyed: function destroyed () { + registerInstance(this); + } + }); -/* */ + Object.defineProperty(Vue.prototype, '$router', { + get: function get () { return this._routerRoot._router } + }); -// $flow-disable-line -var regexpCompileCache = Object.create(null); + Object.defineProperty(Vue.prototype, '$routerLayer', { + get: function get () { return new VueRouterLayer(this._routerRoot._router, this._routerLayer) } + }); -function fillParams ( - path, - params, - routeMsg -) { - params = params || {}; - try { - var filler = - regexpCompileCache[path] || - (regexpCompileCache[path] = pathToRegexp_1.compile(path)); + Object.defineProperty(Vue.prototype, '$route', { + get: function get () { return this._routerRoot._routes[this._routerLayer] } + }); - // Fix #2505 resolving asterisk routes { name: 'not-found', params: { pathMatch: '/not-found' }} - if (params.pathMatch) { params[0] = params.pathMatch; } + Vue.component('RouterView', View); + Vue.component('RouterLink', Link); - return filler(params, { pretty: true }) - } catch (e) { - { - warn(false, ("missing param for " + routeMsg + ": " + (e.message))); - } - return '' - } finally { - // delete the 0 if it was added - delete params[0]; - } + var strats = Vue.config.optionMergeStrategies; + // use the same hook merging strategy for route hooks + strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created; } /* */ +var inBrowser = typeof window !== 'undefined'; + +/* */ + function createRouteMap ( routes, oldPathList, @@ -1290,64 +1403,6 @@ function normalizePath (path, parent, strict) { /* */ -function normalizeLocation ( - raw, - current, - append, - router -) { - var next = typeof raw === 'string' ? { path: raw } : raw; - // named target - if (next._normalized) { - return next - } else if (next.name) { - return extend({}, raw) - } - - // relative params - if (!next.path && next.params && current) { - next = extend({}, next); - next._normalized = true; - var params = extend(extend({}, current.params), next.params); - if (current.name) { - next.name = current.name; - next.params = params; - } else if (current.matched.length) { - var rawPath = current.matched[current.matched.length - 1].path; - next.path = fillParams(rawPath, params, ("path " + (current.path))); - } else { - warn(false, "relative params navigation requires a current route."); - } - return next - } - - var parsedPath = parsePath(next.path || ''); - var basePath = (current && current.path) || '/'; - var path = parsedPath.path - ? resolvePath(parsedPath.path, basePath, append || next.append) - : basePath; - - var query = resolveQuery( - parsedPath.query, - next.query, - router && router.options.parseQuery - ); - - var hash = next.hash || parsedPath.hash; - if (hash && hash.charAt(0) !== '#') { - hash = "#" + hash; - } - - return { - _normalized: true, - path: path, - query: query, - hash: hash - } -} - -/* */ - function createMatcher ( @@ -1585,20 +1640,27 @@ function handleScroll ( // wait until re-render finishes before scrolling router.app.$nextTick(function () { var position = getScrollPosition(); - var shouldScroll = behavior.call(router, to, from, isPop ? position : null); + var shouldScroll = behavior.call( + router, + to, + from, + isPop ? position : null + ); if (!shouldScroll) { return } if (typeof shouldScroll.then === 'function') { - shouldScroll.then(function (shouldScroll) { - scrollToPosition((shouldScroll), position); - }).catch(function (err) { - { - assert(false, err.toString()); - } - }); + shouldScroll + .then(function (shouldScroll) { + scrollToPosition((shouldScroll), position); + }) + .catch(function (err) { + { + assert(false, err.toString()); + } + }); } else { scrollToPosition(shouldScroll, position); } @@ -1654,12 +1716,22 @@ function isNumber (v) { return typeof v === 'number' } +var hashStartsWithNumberRE = /^#\d/; + function scrollToPosition (shouldScroll, position) { var isObject = typeof shouldScroll === 'object'; if (isObject && typeof shouldScroll.selector === 'string') { - var el = document.querySelector(shouldScroll.selector); + // getElementById would still fail if the selector contains a more complicated query like #main[data-attr] + // but at the same time, it doesn't make much sense to select an element with an id and an extra selector + var el = hashStartsWithNumberRE.test(shouldScroll.selector) // $flow-disable-line + ? document.getElementById(shouldScroll.selector.slice(1)) // $flow-disable-line + : document.querySelector(shouldScroll.selector); + if (el) { - var offset = shouldScroll.offset && typeof shouldScroll.offset === 'object' ? shouldScroll.offset : {}; + var offset = + shouldScroll.offset && typeof shouldScroll.offset === 'object' + ? shouldScroll.offset + : {}; offset = normalizeOffset(offset); position = getElementPosition(el, offset); } else if (isValidPosition(shouldScroll)) { @@ -1710,17 +1782,17 @@ function setStateKey (key) { _key = key; } -function pushState (url, replace) { +function pushState (url, state, replace) { saveScrollPosition(); // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls var history = window.history; try { if (replace) { - history.replaceState({ key: _key }, '', url); + history.replaceState({ key: _key, state: state }, '', url); } else { _key = genKey(); - history.pushState({ key: _key }, '', url); + history.pushState({ key: _key, state: state }, '', url); } } catch (e) { window.location[replace ? 'replace' : 'assign'](url); @@ -1859,13 +1931,26 @@ function once (fn) { } } +var NavigationDuplicated = /*@__PURE__*/(function (Error) { + function NavigationDuplicated () { + Error.call(this, 'Navigating to current location is not allowed'); + this.name = 'NavigationDuplicated'; + } + + if ( Error ) NavigationDuplicated.__proto__ = Error; + NavigationDuplicated.prototype = Object.create( Error && Error.prototype ); + NavigationDuplicated.prototype.constructor = NavigationDuplicated; + + return NavigationDuplicated; +}(Error)); + /* */ var History = function History (router, base) { this.router = router; this.base = normalizeBase(base); // start with a route object that stands for "nowhere" - this.current = START; + this.current = [START]; this.pending = null; this.ready = false; this.readyCbs = []; @@ -1892,39 +1977,64 @@ History.prototype.onError = function onError (errorCb) { this.errorCbs.push(errorCb); }; -History.prototype.transitionTo = function transitionTo (location, onComplete, onAbort) { - var this$1 = this; +History.prototype.getClosestCurrent = function getClosestCurrent (n) { + if (n >= this.current.length) { + return this.current[this.current.length - 1] + } + return this.current[n] +}; - var route = this.router.match(location, this.current); - this.confirmTransition(route, function () { - this$1.updateRoute(route); - onComplete && onComplete(route); - this$1.ensureURL(); +History.prototype.transitionTo = function transitionTo ( + locations, + onComplete, + onAbort +) { + var this$1 = this; - // fire ready cbs once - if (!this$1.ready) { - this$1.ready = true; - this$1.readyCbs.forEach(function (cb) { cb(route); }); - } - }, function (err) { - if (onAbort) { - onAbort(err); - } - if (err && !this$1.ready) { - this$1.ready = true; - this$1.readyErrorCbs.forEach(function (cb) { cb(err); }); + var routes = locations.map(function (location, i) { return this$1.router.match(location, this$1.getClosestCurrent(i)); }); + this.confirmTransition( + routes, + function (equalLayers) { + this$1.updateCurrent(routes, equalLayers); + onComplete && onComplete(routes); + this$1.ensureURL(); + + // fire ready cbs once + if (!this$1.ready) { + this$1.ready = true; + this$1.readyCbs.forEach(function (cb) { + cb(routes); + }); + } + }, + function (err) { + if (onAbort) { + onAbort(err); + } + if (err && !this$1.ready) { + this$1.ready = true; + this$1.readyErrorCbs.forEach(function (cb) { + cb(err); + }); + } } - }); + ); }; -History.prototype.confirmTransition = function confirmTransition (route, onComplete, onAbort) { +History.prototype.confirmTransition = function confirmTransition (routes, onComplete, onAbort) { var this$1 = this; var current = this.current; var abort = function (err) { - if (isError(err)) { + // after merging https://github.com/vuejs/vue-router/pull/2771 we + // When the user navigates through history through back/forward buttons + // we do not want to throw the error. We only throw it if directly calling + // push/replace. That's why it's not included in isError + if (!isExtendedError(NavigationDuplicated, err) && isError(err)) { if (this$1.errorCbs.length) { - this$1.errorCbs.forEach(function (cb) { cb(err); }); + this$1.errorCbs.forEach(function (cb) { + cb(err); + }); } else { warn(false, 'uncaught error during route navigation:'); console.error(err); @@ -1932,16 +2042,28 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl } onAbort && onAbort(err); }; - if ( - isSameRoute(route, current) && - // in the case the route map has been dynamically appended to - route.matched.length === current.matched.length - ) { + + var equalLayers = 0; + for (equalLayers = 0; equalLayers < routes.length && equalLayers < current.length; equalLayers++) { + if ( + !isSameRoute(routes[equalLayers], current[equalLayers]) || + // in the case the route map has been dynamically appended to + routes[equalLayers].matched.length !== current[equalLayers].matched.length + ) { + break + } + } + + if (equalLayers === routes.length && equalLayers === current.length) { this.ensureURL(); - return abort() + return abort(new NavigationDuplicated(routes)) } - var ref = resolveQueue(this.current.matched, route.matched); + var ref = resolveQueues( + current, + routes, + equalLayers + ); var updated = ref.updated; var deactivated = ref.deactivated; var activated = ref.activated; @@ -1959,23 +2081,21 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl resolveAsyncComponents(activated) ); - this.pending = route; + this.pending = routes; var iterator = function (hook, next) { - if (this$1.pending !== route) { + if (this$1.pending !== routes) { return abort() } try { - hook(route, current, function (to) { + hook(routes, current, function (to) { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this$1.ensureURL(true); abort(to); } else if ( typeof to === 'string' || - (typeof to === 'object' && ( - typeof to.path === 'string' || - typeof to.name === 'string' - )) + (typeof to === 'object' && + (typeof to.path === 'string' || typeof to.name === 'string')) ) { // next('/') or next({ path: '/' }) -> redirect abort(); @@ -1996,32 +2116,34 @@ History.prototype.confirmTransition = function confirmTransition (route, onCompl runQueue(queue, iterator, function () { var postEnterCbs = []; - var isValid = function () { return this$1.current === route; }; + var isValid = function () { return this$1.current === routes; }; // wait until async components are resolved before // extracting in-component enter guards var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid); var queue = enterGuards.concat(this$1.router.resolveHooks); runQueue(queue, iterator, function () { - if (this$1.pending !== route) { + if (this$1.pending !== routes) { return abort() } this$1.pending = null; - onComplete(route); + onComplete(equalLayers); if (this$1.router.app) { this$1.router.app.$nextTick(function () { - postEnterCbs.forEach(function (cb) { cb(); }); + postEnterCbs.forEach(function (cb) { + cb(); + }); }); } }); }); }; -History.prototype.updateRoute = function updateRoute (route) { +History.prototype.updateCurrent = function updateCurrent (routes, equalLayers) { var prev = this.current; - this.current = route; - this.cb && this.cb(route); + this.current = routes; + this.cb && this.cb(routes, equalLayers); this.router.afterHooks.forEach(function (hook) { - hook && hook(route, prev); + hook && hook(routes, prev); }); }; @@ -2045,6 +2167,35 @@ function normalizeBase (base) { return base.replace(/\/$/, '') } +function resolveQueues ( + current, + next, + equalLayers +) { + var ref, ref$1, ref$2, ref$3, ref$4; + + var res = { + updated: [], + activated: [], + deactivated: [] + }; + + var min = Math.min(current.length, next.length); + for (var i = equalLayers; i < min; i++) { + var r = resolveQueue(current[i].matched, next[i].matched) + (ref = res.updated).push.apply(ref, r.updated) + (ref$1 = res.activated).push.apply(ref$1, r.activated) + (ref$2 = res.deactivated).push.apply(ref$2, r.deactivated); + } + for (var i$1 = min; i$1 < current.length; i$1++) { + (ref$3 = res.deactivated).push.apply(ref$3, current[i$1].matched); + } + for (var i$2 = min; i$2 < next.length; i$2++) { + (ref$4 = res.activated).push.apply(ref$4, next[i$2].matched); + } + return res +} + function resolveQueue ( current, next @@ -2112,9 +2263,13 @@ function extractEnterGuards ( cbs, isValid ) { - return extractGuards(activated, 'beforeRouteEnter', function (guard, _, match, key) { - return bindEnterGuard(guard, match, key, cbs, isValid) - }) + return extractGuards( + activated, + 'beforeRouteEnter', + function (guard, _, match, key) { + return bindEnterGuard(guard, match, key, cbs, isValid) + } + ) } function bindEnterGuard ( @@ -2185,7 +2340,11 @@ var HTML5History = /*@__PURE__*/(function (History$$1) { return } - this$1.transitionTo(location, function (route) { + var locations = [location]; + if (window.history.state.state) { + locations = window.history.state.state; + } + this$1.transitionTo(locations, function (route) { if (supportsScroll) { handleScroll(router, route, current, true); } @@ -2201,34 +2360,48 @@ var HTML5History = /*@__PURE__*/(function (History$$1) { window.history.go(n); }; - HTML5History.prototype.push = function push (location, onComplete, onAbort) { + HTML5History.prototype.navigateAllLayers = function navigateAllLayers (locations, push, onComplete, onAbort) { var this$1 = this; var ref = this; var fromRoute = ref.current; - this.transitionTo(location, function (route) { - pushState(cleanPath(this$1.base + route.fullPath)); + this.transitionTo(locations, function (routes) { + this$1.ensureURL(push); + var route = this$1.current[this$1.current.length - 1]; handleScroll(this$1.router, route, fromRoute, false); onComplete && onComplete(route); }, onAbort); }; - HTML5History.prototype.replace = function replace (location, onComplete, onAbort) { - var this$1 = this; + HTML5History.prototype.navigateLastLayer = function navigateLastLayer (location, push, onComplete, onAbort) { + var locations = this.current.slice(0, -1).map(function (r) { return r.fullPath; }).concat( [location] + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; - var ref = this; - var fromRoute = ref.current; - this.transitionTo(location, function (route) { - replaceState(cleanPath(this$1.base + route.fullPath)); - handleScroll(this$1.router, route, fromRoute, false); - onComplete && onComplete(route); - }, onAbort); + HTML5History.prototype.navigateLayer = function navigateLayer (layer, location, push, onComplete, onAbort) { + var locations = this.current.slice(0, layer).map(function (r) { return r.fullPath; }).concat( [location], + this.current.slice(layer + 1).map(function (r) { return r.fullPath; }) + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; + + HTML5History.prototype.navigateAddLayer = function navigateAddLayer (location, push, onComplete, onAbort) { + var locations = this.current.map(function (r) { return r.fullPath; }).concat( [location] + ); + this.navigateAllLayers(locations, push, onComplete, onAbort); + }; + + HTML5History.prototype.navigateRemoveLayer = function navigateRemoveLayer (location, push, onComplete, onAbort) { + var locations = this.current.slice(0, -1).map(function (r) { return r.fullPath; }); + this.navigateAllLayers(locations, push, onComplete, onAbort); }; HTML5History.prototype.ensureURL = function ensureURL (push) { - if (getLocation(this.base) !== this.current.fullPath) { - var current = cleanPath(this.base + this.current.fullPath); - push ? pushState(current) : replaceState(current); + var route = this.current[this.current.length - 1]; + if (getLocation(this.base) !== route.fullPath) { + var path = cleanPath(this.base + route.fullPath); + pushState(path, this.current.map(function (r) { return r.fullPath; }), !push); } }; @@ -2416,20 +2589,28 @@ var AbstractHistory = /*@__PURE__*/(function (History$$1) { AbstractHistory.prototype.push = function push (location, onComplete, onAbort) { var this$1 = this; - this.transitionTo(location, function (route) { - this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route); - this$1.index++; - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + function (route) { + this$1.stack = this$1.stack.slice(0, this$1.index + 1).concat(route); + this$1.index++; + onComplete && onComplete(route); + }, + onAbort + ); }; AbstractHistory.prototype.replace = function replace (location, onComplete, onAbort) { var this$1 = this; - this.transitionTo(location, function (route) { - this$1.stack = this$1.stack.slice(0, this$1.index).concat(route); - onComplete && onComplete(route); - }, onAbort); + this.transitionTo( + location, + function (route) { + this$1.stack = this$1.stack.slice(0, this$1.index).concat(route); + onComplete && onComplete(route); + }, + onAbort + ); }; AbstractHistory.prototype.go = function go (n) { @@ -2440,10 +2621,18 @@ var AbstractHistory = /*@__PURE__*/(function (History$$1) { return } var route = this.stack[targetIndex]; - this.confirmTransition(route, function () { - this$1.index = targetIndex; - this$1.updateRoute(route); - }); + this.confirmTransition( + route, + function () { + this$1.index = targetIndex; + this$1.updateRoute(route); + }, + function (err) { + if (isExtendedError(NavigationDuplicated, err)) { + this$1.index = targetIndex; + } + } + ); }; AbstractHistory.prototype.getCurrentLocation = function getCurrentLocation () { @@ -2547,7 +2736,7 @@ VueRouter.prototype.init = function init (app /* Vue component instance */) { var history = this.history; if (history instanceof HTML5History) { - history.transitionTo(history.getCurrentLocation()); + history.transitionTo([history.getCurrentLocation()]); } else if (history instanceof HashHistory) { var setupHashListener = function () { history.setupListeners(); @@ -2559,9 +2748,21 @@ VueRouter.prototype.init = function init (app /* Vue component instance */) { ); } - history.listen(function (route) { + history.listen(function (routes, equalLayers) { this$1.apps.forEach(function (app) { - app._route = route; + // Mutate only the changed routes, so we don't trigger unnecessary updates. + // Changed elements + for (var i = equalLayers; i < app._routes.length && i < routes.length; i++) { + app.$set(app._routes, i, routes[i]); + } + // Added elements + for (var i$1 = app._routes.length; i$1 < routes.length; i$1++) { + app._routes.push(routes[i$1]); + } + // Removed + while (app._routes.length > routes.length) { + app._routes.pop(); + } }); }); }; @@ -2587,11 +2788,43 @@ VueRouter.prototype.onError = function onError (errorCb) { }; VueRouter.prototype.push = function push (location, onComplete, onAbort) { - this.history.push(location, onComplete, onAbort); + this.history.navigateLastLayer(location, true, onComplete, onAbort); }; VueRouter.prototype.replace = function replace (location, onComplete, onAbort) { - this.history.replace(location, onComplete, onAbort); + this.history.navigateLastLayer(location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushAddLayer = function pushAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceAddLayer = function replaceAddLayer (location, onComplete, onAbort) { + this.history.navigateAddLayer(location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushRemoveLayer = function pushRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceRemoveLayer = function replaceRemoveLayer (onComplete, onAbort) { + this.history.navigateRemoveLayer(false, onComplete, onAbort); +}; + +VueRouter.prototype.pushLayer = function pushLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceLayer = function replaceLayer (layer, location, onComplete, onAbort) { + this.history.navigateLayer(layer, location, false, onComplete, onAbort); +}; + +VueRouter.prototype.pushAllLayers = function pushAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, true, onComplete, onAbort); +}; + +VueRouter.prototype.replaceAllLayers = function replaceAllLayers (locations, onComplete, onAbort) { + this.history.navigateAllLayers(locations, false, onComplete, onAbort); }; VueRouter.prototype.go = function go (n) { diff --git a/dist/vue-router.min.js b/dist/vue-router.min.js index 7f0392bb2..0108872c0 100644 --- a/dist/vue-router.min.js +++ b/dist/vue-router.min.js @@ -3,4 +3,4 @@ * (c) 2019 Evan You * @license MIT */ -var t,e;t=this,e=function(){"use strict";function t(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function e(t,e){for(var r in e)t[r]=e[r];return t}var r={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"}},render:function(t,r){var n=r.props,o=r.children,i=r.parent,a=r.data;a.routerView=!0;for(var u=i.$createElement,c=n.name,s=i.$route,p=i._routerViewCache||(i._routerViewCache={}),f=0,h=!1;i&&i._routerRoot!==i;){var l=i.$vnode&&i.$vnode.data;l&&(l.routerView&&f++,l.keepAlive&&i._inactive&&(h=!0)),i=i.$parent}if(a.routerViewDepth=f,h)return u(p[c],a,o);var d=s.matched[f];if(!d)return p[c]=null,u();var v=p[c]=d.components[c];a.registerRouteInstance=function(t,e){var r=d.instances[c];(e&&r!==t||!e&&r===t)&&(d.instances[c]=e)},(a.hook||(a.hook={})).prepatch=function(t,e){d.instances[c]=e.componentInstance},a.hook.init=function(t){t.data.keepAlive&&t.componentInstance&&t.componentInstance!==d.instances[c]&&(d.instances[c]=t.componentInstance)};var y=a.props=function(t,e){switch(typeof e){case"undefined":return;case"object":return e;case"function":return e(t);case"boolean":return e?t.params:void 0}}(s,d.props&&d.props[c]);if(y){y=a.props=e({},y);var m=a.attrs=a.attrs||{};for(var g in y)v.props&&g in v.props||(m[g]=y[g],delete y[g])}return u(v,a,o)}},n=/[!'()*]/g,o=function(t){return"%"+t.charCodeAt(0).toString(16)},i=/%2C/g,a=function(t){return encodeURIComponent(t).replace(n,o).replace(i,",")},u=decodeURIComponent;function c(t){var e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach(function(t){var r=t.replace(/\+/g," ").split("="),n=u(r.shift()),o=r.length>0?u(r.join("=")):null;void 0===e[n]?e[n]=o:Array.isArray(e[n])?e[n].push(o):e[n]=[e[n],o]}),e):e}function s(t){var e=t?Object.keys(t).map(function(e){var r=t[e];if(void 0===r)return"";if(null===r)return a(e);if(Array.isArray(r)){var n=[];return r.forEach(function(t){void 0!==t&&(null===t?n.push(a(e)):n.push(a(e)+"="+a(t)))}),n.join("&")}return a(e)+"="+a(r)}).filter(function(t){return t.length>0}).join("&"):null;return e?"?"+e:""}var p=/\/?$/;function f(t,e,r,n){var o=n&&n.options.stringifyQuery,i=e.query||{};try{i=h(i)}catch(t){}var a={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:i,params:e.params||{},fullPath:v(e,o),matched:t?d(t):[]};return r&&(a.redirectedFrom=v(r,o)),Object.freeze(a)}function h(t){if(Array.isArray(t))return t.map(h);if(t&&"object"==typeof t){var e={};for(var r in t)e[r]=h(t[r]);return e}return t}var l=f(null,{path:"/"});function d(t){for(var e=[];t;)e.unshift(t),t=t.parent;return e}function v(t,e){var r=t.path,n=t.query;void 0===n&&(n={});var o=t.hash;return void 0===o&&(o=""),(r||"/")+(e||s)(n)+o}function y(t,e){return e===l?t===e:!!e&&(t.path&&e.path?t.path.replace(p,"")===e.path.replace(p,"")&&t.hash===e.hash&&m(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&m(t.query,e.query)&&m(t.params,e.params)))}function m(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var r=Object.keys(t),n=Object.keys(e);return r.length===n.length&&r.every(function(r){var n=t[r],o=e[r];return"object"==typeof n&&"object"==typeof o?m(n,o):String(n)===String(o)})}var g,b=[String,Object],w=[String,Array],x={name:"RouterLink",props:{to:{type:b,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,event:{type:w,default:"click"}},render:function(t){var r=this,n=this.$router,o=this.$route,i=n.resolve(this.to,o,this.append),a=i.location,u=i.route,c=i.href,s={},h=n.options.linkActiveClass,l=n.options.linkExactActiveClass,d=null==h?"router-link-active":h,v=null==l?"router-link-exact-active":l,m=null==this.activeClass?d:this.activeClass,g=null==this.exactActiveClass?v:this.exactActiveClass,b=a.path?f(null,a,null,n):u;s[g]=y(o,b),s[m]=this.exact?s[g]:function(t,e){return 0===t.path.replace(p,"/").indexOf(e.path.replace(p,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(var r in e)if(!(r in t))return!1;return!0}(t.query,e.query)}(o,b);var w=function(t){k(t)&&(r.replace?n.replace(a):n.push(a))},x={click:k};Array.isArray(this.event)?this.event.forEach(function(t){x[t]=w}):x[this.event]=w;var R={class:s};if("a"===this.tag)R.on=x,R.attrs={href:c};else{var E=function t(e){if(e)for(var r,n=0;n=0&&(e=t.slice(n),t=t.slice(0,n));var o=t.indexOf("?");return o>=0&&(r=t.slice(o+1),t=t.slice(0,o)),{path:t,query:r,hash:e}}(i.path||""),p=r&&r.path||"/",f=s.path?E(s.path,p,n||i.append):p,h=function(t,e,r){void 0===e&&(e={});var n,o=r||c;try{n=o(t||"")}catch(t){n={}}for(var i in e)n[i]=e[i];return n}(s.query,i.query,o&&o.options.parseQuery),l=i.hash||s.hash;return l&&"#"!==l.charAt(0)&&(l="#"+l),{_normalized:!0,path:f,query:h,hash:l}}function N(t,e){var r=F(t),n=r.pathList,o=r.pathMap,i=r.nameMap;function a(t,r,a){var u=J(t,r,!1,e),s=u.name;if(s){var p=i[s];if(!p)return c(null,u);var f=p.regex.keys.filter(function(t){return!t.optional}).map(function(t){return t.name});if("object"!=typeof u.params&&(u.params={}),r&&"object"==typeof r.params)for(var h in r.params)!(h in u.params)&&f.indexOf(h)>-1&&(u.params[h]=r.params[h]);return u.path=D(p.path,u.params),c(p,u,a)}if(u.path){u.params={};for(var l=0;l=t.length?r():t[o]?e(t[o],function(){n(o+1)}):n(o+1)};n(0)}function ht(e){return function(r,n,o){var i=!1,a=0,u=null;lt(e,function(e,r,n,c){if("function"==typeof e&&void 0===e.cid){i=!0,a++;var s,p=yt(function(t){var r;((r=t).__esModule||vt&&"Module"===r[Symbol.toStringTag])&&(t=t.default),e.resolved="function"==typeof t?t:g.extend(t),n.components[c]=t,--a<=0&&o()}),f=yt(function(e){var r="Failed to resolve async component "+c+": "+e;u||(u=t(e)?e:new Error(r),o(u))});try{s=e(p,f)}catch(t){f(t)}if(s)if("function"==typeof s.then)s.then(p,f);else{var h=s.component;h&&"function"==typeof h.then&&h.then(p,f)}}}),i||o()}}function lt(t,e){return dt(t.map(function(t){return Object.keys(t.components).map(function(r){return e(t.components[r],t.instances[r],t,r)})}))}function dt(t){return Array.prototype.concat.apply([],t)}var vt="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function yt(t){var e=!1;return function(){for(var r=[],n=arguments.length;n--;)r[n]=arguments[n];if(!e)return e=!0,t.apply(this,r)}}var mt=function(t,e){this.router=t,this.base=function(t){if(!t)if(R){var e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";"/"!==t.charAt(0)&&(t="/"+t);return t.replace(/\/$/,"")}(e),this.current=l,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]};function gt(t,e,r,n){var o=lt(t,function(t,n,o,i){var a=function(t,e){"function"!=typeof t&&(t=g.extend(t));return t.options[e]}(t,e);if(a)return Array.isArray(a)?a.map(function(t){return r(t,n,o,i)}):r(a,n,o,i)});return dt(n?o.reverse():o)}function bt(t,e){if(e)return function(){return t.apply(e,arguments)}}mt.prototype.listen=function(t){this.cb=t},mt.prototype.onReady=function(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))},mt.prototype.onError=function(t){this.errorCbs.push(t)},mt.prototype.transitionTo=function(t,e,r){var n=this,o=this.router.match(t,this.current);this.confirmTransition(o,function(){n.updateRoute(o),e&&e(o),n.ensureURL(),n.ready||(n.ready=!0,n.readyCbs.forEach(function(t){t(o)}))},function(t){r&&r(t),t&&!n.ready&&(n.ready=!0,n.readyErrorCbs.forEach(function(e){e(t)}))})},mt.prototype.confirmTransition=function(e,r,n){var o=this,i=this.current,a=function(e){t(e)&&(o.errorCbs.length?o.errorCbs.forEach(function(t){t(e)}):console.error(e)),n&&n(e)};if(y(e,i)&&e.matched.length===i.matched.length)return this.ensureURL(),a();var u=function(t,e){var r,n=Math.max(t.length,e.length);for(r=0;r-1?decodeURI(t.slice(0,n))+t.slice(n):decodeURI(t)}else r>-1&&(t=decodeURI(t.slice(0,r))+t.slice(r));return t}function Ot(t){var e=window.location.href,r=e.indexOf("#");return(r>=0?e.slice(0,r):e)+"#"+t}function At(t){ot?st(Ot(t)):window.location.hash=t}function Ct(t){ot?pt(Ot(t)):window.location.replace(Ot(t))}var jt=function(t){function e(e,r){t.call(this,e,r),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,r){var n=this;this.transitionTo(t,function(t){n.stack=n.stack.slice(0,n.index+1).concat(t),n.index++,e&&e(t)},r)},e.prototype.replace=function(t,e,r){var n=this;this.transitionTo(t,function(t){n.stack=n.stack.slice(0,n.index).concat(t),e&&e(t)},r)},e.prototype.go=function(t){var e=this,r=this.index+t;if(!(r<0||r>=this.stack.length)){var n=this.stack[r];this.confirmTransition(n,function(){e.index=r,e.updateRoute(n)})}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(mt),_t=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=N(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!ot&&!1!==t.fallback,this.fallback&&(e="hash"),R||(e="abstract"),this.mode=e,e){case"history":this.history=new wt(this,t.base);break;case"hash":this.history=new kt(this,t.base,this.fallback);break;case"abstract":this.history=new jt(this,t.base)}},Tt={currentRoute:{configurable:!0}};function St(t,e){return t.push(e),function(){var r=t.indexOf(e);r>-1&&t.splice(r,1)}}return _t.prototype.match=function(t,e,r){return this.matcher.match(t,e,r)},Tt.currentRoute.get=function(){return this.history&&this.history.current},_t.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",function(){var r=e.apps.indexOf(t);r>-1&&e.apps.splice(r,1),e.app===t&&(e.app=e.apps[0]||null)}),!this.app){this.app=t;var r=this.history;if(r instanceof wt)r.transitionTo(r.getCurrentLocation());else if(r instanceof kt){var n=function(){r.setupListeners()};r.transitionTo(r.getCurrentLocation(),n,n)}r.listen(function(t){e.apps.forEach(function(e){e._route=t})})}},_t.prototype.beforeEach=function(t){return St(this.beforeHooks,t)},_t.prototype.beforeResolve=function(t){return St(this.resolveHooks,t)},_t.prototype.afterEach=function(t){return St(this.afterHooks,t)},_t.prototype.onReady=function(t,e){this.history.onReady(t,e)},_t.prototype.onError=function(t){this.history.onError(t)},_t.prototype.push=function(t,e,r){this.history.push(t,e,r)},_t.prototype.replace=function(t,e,r){this.history.replace(t,e,r)},_t.prototype.go=function(t){this.history.go(t)},_t.prototype.back=function(){this.go(-1)},_t.prototype.forward=function(){this.go(1)},_t.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map(function(t){return Object.keys(t.components).map(function(e){return t.components[e]})})):[]},_t.prototype.resolve=function(t,e,r){var n=J(t,e=e||this.history.current,r,this),o=this.match(n,e),i=o.redirectedFrom||o.fullPath;return{location:n,route:o,href:function(t,e,r){var n="hash"===r?"#"+e:e;return t?O(t+"/"+n):n}(this.history.base,i,this.mode),normalizedTo:n,resolved:o}},_t.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==l&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(_t.prototype,Tt),_t.install=function t(e){if(!t.installed||g!==e){t.installed=!0,g=e;var n=function(t){return void 0!==t},o=function(t,e){var r=t.$options._parentVnode;n(r)&&n(r=r.data)&&n(r=r.registerRouteInstance)&&r(t,e)};e.mixin({beforeCreate:function(){n(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),e.util.defineReactive(this,"_route",this._router.history.current)):this._routerRoot=this.$parent&&this.$parent._routerRoot||this,o(this,this)},destroyed:function(){o(this)}}),Object.defineProperty(e.prototype,"$router",{get:function(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$route",{get:function(){return this._routerRoot._route}}),e.component("RouterView",r),e.component("RouterLink",x);var i=e.config.optionMergeStrategies;i.beforeRouteEnter=i.beforeRouteLeave=i.beforeRouteUpdate=i.created}},_t.version="3.0.7",R&&window.Vue&&window.Vue.use(_t),_t},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueRouter=e(); \ No newline at end of file +var t,e;t=this,e=function(){"use strict";function t(t){return Object.prototype.toString.call(t).indexOf("Error")>-1}function e(t,e){return e instanceof t||e&&e.name===t.name}function r(t,e){for(var r in e)t[r]=e[r];return t}var n={name:"RouterView",functional:!0,props:{name:{type:String,default:"default"},nextLayer:{type:Boolean,default:!1}},render:function(t,e){var n=e.props,o=e.children,a=e.parent,i=e.data;i.routerView=!0;var u=a.$createElement,c=n.name,s=a._routerViewCache||(a._routerViewCache={}),p=a._routerLayer+(n.nextLayer?1:0);if(a._routerRoot._routes.length<=p)return s[c]=null,u();for(var f=a._routerRoot._routes[p],h=0,l=!1;a&&a._routerRoot!==a;){var y=a.$vnode&&a.$vnode.data;y&&(y.routerView&&h++,y.keepAlive&&a._inactive&&(l=!0)),a=a.$parent}if(i.routerViewDepth=h,l)return u(s[c],i,o);var d=f.matched[h];if(!d)return s[c]=null,u();var v=s[c]=d.components[c];i.registerRouteInstance=function(t,e){var r=d.instances[c];(e&&r!==t||!e&&r===t)&&(d.instances[c]=e),t._routerLayer=p},(i.hook||(i.hook={})).prepatch=function(t,e){d.instances[c]=e.componentInstance},i.hook.init=function(t){t.data.keepAlive&&t.componentInstance&&t.componentInstance!==d.instances[c]&&(d.instances[c]=t.componentInstance)};var m=i.props=function(t,e){switch(typeof e){case"undefined":return;case"object":return e;case"function":return e(t);case"boolean":return e?t.params:void 0}}(f,d.props&&d.props[c]);if(m){m=i.props=r({},m);var g=i.attrs=i.attrs||{};for(var w in m)v.props&&w in v.props||(g[w]=m[w],delete m[w])}return u(v,i,o)}},o=/[!'()*]/g,a=function(t){return"%"+t.charCodeAt(0).toString(16)},i=/%2C/g,u=function(t){return encodeURIComponent(t).replace(o,a).replace(i,",")},c=decodeURIComponent;function s(t){var e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach(function(t){var r=t.replace(/\+/g," ").split("="),n=c(r.shift()),o=r.length>0?c(r.join("=")):null;void 0===e[n]?e[n]=o:Array.isArray(e[n])?e[n].push(o):e[n]=[e[n],o]}),e):e}function p(t){var e=t?Object.keys(t).map(function(e){var r=t[e];if(void 0===r)return"";if(null===r)return u(e);if(Array.isArray(r)){var n=[];return r.forEach(function(t){void 0!==t&&(null===t?n.push(u(e)):n.push(u(e)+"="+u(t)))}),n.join("&")}return u(e)+"="+u(r)}).filter(function(t){return t.length>0}).join("&"):null;return e?"?"+e:""}var f=/\/?$/;function h(t,e,r,n){var o=n&&n.options.stringifyQuery,a=e.query||{};try{a=l(a)}catch(t){}var i={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:a,params:e.params||{},fullPath:v(e,o),matched:t?d(t):[]};return r&&(i.redirectedFrom=v(r,o)),Object.freeze(i)}function l(t){if(Array.isArray(t))return t.map(l);if(t&&"object"==typeof t){var e={};for(var r in t)e[r]=l(t[r]);return e}return t}var y=h(null,{path:"/"});function d(t){for(var e=[];t;)e.unshift(t),t=t.parent;return e}function v(t,e){var r=t.path,n=t.query;void 0===n&&(n={});var o=t.hash;return void 0===o&&(o=""),(r||"/")+(e||p)(n)+o}function m(t,e){return e===y?t===e:!!e&&(t.path&&e.path?t.path.replace(f,"")===e.path.replace(f,"")&&t.hash===e.hash&&g(t.query,e.query):!(!t.name||!e.name)&&(t.name===e.name&&t.hash===e.hash&&g(t.query,e.query)&&g(t.params,e.params)))}function g(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var r=Object.keys(t),n=Object.keys(e);return r.length===n.length&&r.every(function(r){var n=t[r],o=e[r];return"object"==typeof n&&"object"==typeof o?g(n,o):String(n)===String(o)})}function w(t,e,r){var n=t.charAt(0);if("/"===n)return t;if("?"===n||"#"===n)return e+t;var o=e.split("/");r&&o[o.length-1]||o.pop();for(var a=t.replace(/^\//,"").split("/"),i=0;i=0&&(e=t.slice(n),t=t.slice(0,n));var o=t.indexOf("?");return o>=0&&(r=t.slice(o+1),t=t.slice(0,o)),{path:t,query:r,hash:e}}(a.path||""),p=e&&e.path||"/",f=c.path?w(c.path,p,n||a.append):p,h=function(t,e,r){void 0===e&&(e={});var n,o=r||s;try{n=o(t||"")}catch(t){n={}}for(var a in e)n[a]=e[a];return n}(c.query,a.query,o&&o.options.parseQuery),l=a.hash||c.hash;return l&&"#"!==l.charAt(0)&&(l="#"+l),{_normalized:!0,path:f,query:h,hash:l}}var V,H=[String,Object],z=[String,Array],F={name:"RouterLink",props:{to:{type:H,required:!0},tag:{type:String,default:"a"},exact:Boolean,append:Boolean,replace:Boolean,addLayer:Boolean,removeLayer:Boolean,activeClass:String,exactActiveClass:String,event:{type:z,default:"click"}},render:function(t){var e=this,n=this.$router,o=this.$route,a=n.resolve(this.to,o,this.append),i=a.location,u=a.route,c=a.href,s={},p=n.options.linkActiveClass,l=n.options.linkExactActiveClass,y=null==p?"router-link-active":p,d=null==l?"router-link-exact-active":l,v=null==this.activeClass?y:this.activeClass,g=null==this.exactActiveClass?d:this.exactActiveClass,w=u.redirectedFrom?h(null,B(u.redirectedFrom),null,n):u;s[g]=m(o,w),s[v]=this.exact?s[g]:function(t,e){return 0===t.path.replace(f,"/").indexOf(e.path.replace(f,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(var r in e)if(!(r in t))return!1;return!0}(t.query,e.query)}(o,w);var b=function(t){D(t)&&(e.replace?e.addLayer?n.replaceAddLayer(i):e.removeLayer?n.replaceRemoveLayer():n.replaceLayer(e._routerLayer,i):e.addLayer?n.pushAddLayer(i):e.removeLayer?n.pushRemoveLayer():n.pushLayer(e._routerLayer,i))},L={click:D};Array.isArray(this.event)?this.event.forEach(function(t){L[t]=b}):L[this.event]=b;var x={class:s};if("a"===this.tag)x.on=L,x.attrs={href:c};else{var R=function t(e){if(e)for(var r,n=0;n-1&&(u.params[h]=r.params[h]);return u.path=M(p.path,u.params),c(p,u,i)}if(u.path){u.params={};for(var l=0;l=t.length?r():t[o]?e(t[o],function(){n(o+1)}):n(o+1)};n(0)}function yt(e){return function(r,n,o){var a=!1,i=0,u=null;dt(e,function(e,r,n,c){if("function"==typeof e&&void 0===e.cid){a=!0,i++;var s,p=gt(function(t){var r;((r=t).__esModule||mt&&"Module"===r[Symbol.toStringTag])&&(t=t.default),e.resolved="function"==typeof t?t:V.extend(t),n.components[c]=t,--i<=0&&o()}),f=gt(function(e){var r="Failed to resolve async component "+c+": "+e;u||(u=t(e)?e:new Error(r),o(u))});try{s=e(p,f)}catch(t){f(t)}if(s)if("function"==typeof s.then)s.then(p,f);else{var h=s.component;h&&"function"==typeof h.then&&h.then(p,f)}}}),a||o()}}function dt(t,e){return vt(t.map(function(t){return Object.keys(t.components).map(function(r){return e(t.components[r],t.instances[r],t,r)})}))}function vt(t){return Array.prototype.concat.apply([],t)}var mt="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function gt(t){var e=!1;return function(){for(var r=[],n=arguments.length;n--;)r[n]=arguments[n];if(!e)return e=!0,t.apply(this,r)}}var wt=function(t){function e(){t.call(this,"Navigating to current location is not allowed"),this.name="NavigationDuplicated"}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e}(Error),bt=function(t,e){this.router=t,this.base=function(t){if(!t)if(N){var e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";"/"!==t.charAt(0)&&(t="/"+t);return t.replace(/\/$/,"")}(e),this.current=[y],this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[]};function Lt(t,e){var r,n=Math.max(t.length,e.length);for(r=0;r=this.current.length?this.current[this.current.length-1]:this.current[t]},bt.prototype.transitionTo=function(t,e,r){var n=this,o=t.map(function(t,e){return n.router.match(t,n.getClosestCurrent(e))});this.confirmTransition(o,function(t){n.updateCurrent(o,t),e&&e(o),n.ensureURL(),n.ready||(n.ready=!0,n.readyCbs.forEach(function(t){t(o)}))},function(t){r&&r(t),t&&!n.ready&&(n.ready=!0,n.readyErrorCbs.forEach(function(e){e(t)}))})},bt.prototype.confirmTransition=function(r,n,o){var a=this,i=this.current,u=function(r){!e(wt,r)&&t(r)&&(a.errorCbs.length?a.errorCbs.forEach(function(t){t(r)}):console.error(r)),o&&o(r)},c=0;for(c=0;c-1?decodeURI(t.slice(0,n))+t.slice(n):decodeURI(t)}else r>-1&&(t=decodeURI(t.slice(0,r))+t.slice(r));return t}function Ct(t){var e=window.location.href,r=e.indexOf("#");return(r>=0?e.slice(0,r):e)+"#"+t}function jt(t){ut?ht(Ct(t)):window.location.hash=t}function $t(t){ut?ht(Ct(t),!0):window.location.replace(Ct(t))}var Tt=function(t){function r(e,r){t.call(this,e,r),this.stack=[],this.index=-1}return t&&(r.__proto__=t),r.prototype=Object.create(t&&t.prototype),r.prototype.constructor=r,r.prototype.push=function(t,e,r){var n=this;this.transitionTo(t,function(t){n.stack=n.stack.slice(0,n.index+1).concat(t),n.index++,e&&e(t)},r)},r.prototype.replace=function(t,e,r){var n=this;this.transitionTo(t,function(t){n.stack=n.stack.slice(0,n.index).concat(t),e&&e(t)},r)},r.prototype.go=function(t){var r=this,n=this.index+t;if(!(n<0||n>=this.stack.length)){var o=this.stack[n];this.confirmTransition(o,function(){r.index=n,r.updateRoute(o)},function(t){e(wt,t)&&(r.index=n)})}},r.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},r.prototype.ensureURL=function(){},r}(bt),St=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=X(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!ut&&!1!==t.fallback,this.fallback&&(e="hash"),N||(e="abstract"),this.mode=e,e){case"history":this.history=new _t(this,t.base);break;case"hash":this.history=new At(this,t.base,this.fallback);break;case"abstract":this.history=new Tt(this,t.base)}},Pt={currentRoute:{configurable:!0}};function qt(t,e){return t.push(e),function(){var r=t.indexOf(e);r>-1&&t.splice(r,1)}}return St.prototype.match=function(t,e,r){return this.matcher.match(t,e,r)},Pt.currentRoute.get=function(){return this.history&&this.history.current},St.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",function(){var r=e.apps.indexOf(t);r>-1&&e.apps.splice(r,1),e.app===t&&(e.app=e.apps[0]||null)}),!this.app){this.app=t;var r=this.history;if(r instanceof _t)r.transitionTo([r.getCurrentLocation()]);else if(r instanceof At){var n=function(){r.setupListeners()};r.transitionTo(r.getCurrentLocation(),n,n)}r.listen(function(t,r){e.apps.forEach(function(e){for(var n=r;nt.length;)e._routes.pop()})})}},St.prototype.beforeEach=function(t){return qt(this.beforeHooks,t)},St.prototype.beforeResolve=function(t){return qt(this.resolveHooks,t)},St.prototype.afterEach=function(t){return qt(this.afterHooks,t)},St.prototype.onReady=function(t,e){this.history.onReady(t,e)},St.prototype.onError=function(t){this.history.onError(t)},St.prototype.push=function(t,e,r){this.history.navigateLastLayer(t,!0,e,r)},St.prototype.replace=function(t,e,r){this.history.navigateLastLayer(t,!1,e,r)},St.prototype.pushAddLayer=function(t,e,r){this.history.navigateAddLayer(t,!0,e,r)},St.prototype.replaceAddLayer=function(t,e,r){this.history.navigateAddLayer(t,!1,e,r)},St.prototype.pushRemoveLayer=function(t,e){this.history.navigateRemoveLayer(!0,t,e)},St.prototype.replaceRemoveLayer=function(t,e){this.history.navigateRemoveLayer(!1,t,e)},St.prototype.pushLayer=function(t,e,r,n){this.history.navigateLayer(t,e,!0,r,n)},St.prototype.replaceLayer=function(t,e,r,n){this.history.navigateLayer(t,e,!1,r,n)},St.prototype.pushAllLayers=function(t,e,r){this.history.navigateAllLayers(t,!0,e,r)},St.prototype.replaceAllLayers=function(t,e,r){this.history.navigateAllLayers(t,!1,e,r)},St.prototype.go=function(t){this.history.go(t)},St.prototype.back=function(){this.go(-1)},St.prototype.forward=function(){this.go(1)},St.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map(function(t){return Object.keys(t.components).map(function(e){return t.components[e]})})):[]},St.prototype.resolve=function(t,e,r){var n=B(t,e=e||this.history.current,r,this),o=this.match(n,e),a=o.redirectedFrom||o.fullPath;return{location:n,route:o,href:function(t,e,r){var n="hash"===r?"#"+e:e;return t?b(t+"/"+n):n}(this.history.base,a,this.mode),normalizedTo:n,resolved:o}},St.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==y&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(St.prototype,Pt),St.install=function t(e){if(!t.installed||V!==e){t.installed=!0,V=e;var r=function(t){return void 0!==t},o=function(t,e){var n=t.$options._parentVnode;r(n)&&r(n=n.data)&&r(n=n.registerRouteInstance)&&n(t,e)};e.mixin({beforeCreate:function(){r(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),this._routerLayer=0,e.util.defineReactive(this,"_routes",this._router.history.current)):(this._routerRoot=this.$parent&&this.$parent._routerRoot||this,this._routerLayer=this.$parent?this.$parent._routerLayer:0),o(this,this)},destroyed:function(){o(this)}}),Object.defineProperty(e.prototype,"$router",{get:function(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$routerLayer",{get:function(){return new K(this._routerRoot._router,this._routerLayer)}}),Object.defineProperty(e.prototype,"$route",{get:function(){return this._routerRoot._routes[this._routerLayer]}}),e.component("RouterView",n),e.component("RouterLink",F);var a=e.config.optionMergeStrategies;a.beforeRouteEnter=a.beforeRouteLeave=a.beforeRouteUpdate=a.created}},St.version="3.0.7",N&&window.Vue&&window.Vue.use(St),St},"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):t.VueRouter=e(); \ No newline at end of file diff --git a/src/components/link.js b/src/components/link.js index d944e8b82..fcae8627a 100644 --- a/src/components/link.js +++ b/src/components/link.js @@ -22,6 +22,8 @@ export default { exact: Boolean, append: Boolean, replace: Boolean, + addLayer: Boolean, + removeLayer: Boolean, activeClass: String, exactActiveClass: String, event: { @@ -67,9 +69,21 @@ export default { const handler = e => { if (guardEvent(e)) { if (this.replace) { - router.replace(location) + if (this.addLayer) { + router.replaceAddLayer(location) + } else if (this.removeLayer) { + router.replaceRemoveLayer() + } else { + router.replaceLayer(this._routerLayer, location) + } } else { - router.push(location) + if (this.addLayer) { + router.pushAddLayer(location) + } else if (this.removeLayer) { + router.pushRemoveLayer() + } else { + router.pushLayer(this._routerLayer, location) + } } } } diff --git a/src/components/view.js b/src/components/view.js index 3f440d5b4..2d8239116 100644 --- a/src/components/view.js +++ b/src/components/view.js @@ -8,6 +8,10 @@ export default { name: { type: String, default: 'default' + }, + nextLayer: { + type: Boolean, + default: false } }, render (_, { props, children, parent, data }) { @@ -18,9 +22,16 @@ export default { // so that components rendered by router-view can resolve named slots const h = parent.$createElement const name = props.name - const route = parent.$route const cache = parent._routerViewCache || (parent._routerViewCache = {}) + const layer = parent._routerLayer + (props.nextLayer ? 1 : 0) + // render empty node if we don't have this high layers + if (parent._routerRoot._routes.length <= layer) { + cache[name] = null + return h() + } + const route = parent._routerRoot._routes[layer] + // determine current view depth, also check to see if the tree // has been toggled inactive but kept-alive. let depth = 0 @@ -64,6 +75,7 @@ export default { ) { matched.instances[name] = val } + vm._routerLayer = layer } // also register instance in prepatch hook diff --git a/src/history/base.js b/src/history/base.js index 2b12e1967..72ca5078a 100644 --- a/src/history/base.js +++ b/src/history/base.js @@ -16,8 +16,8 @@ import { NavigationDuplicated } from './errors' export class History { router: Router base: string - current: Route - pending: ?Route + current: Array; + pending: ?Array; cb: (r: Route) => void ready: boolean readyCbs: Array @@ -26,8 +26,11 @@ export class History { // implemented by sub-classes +go: (n: number) => void - +push: (loc: RawLocation) => void - +replace: (loc: RawLocation) => void + +navigateAllLayers: (locs: Array, push: boolean) => void + +navigateLastLayer: (loc: RawLocation, push: boolean) => void + +navigateLayer: (layer: number, loc: RawLocation, push: boolean) => void + +navigateAddLayer: (loc: RawLocation, push: boolean) => void + +navigateRemoveLayer: (push: boolean) => void +ensureURL: (push?: boolean) => void +getCurrentLocation: () => string @@ -35,7 +38,7 @@ export class History { this.router = router this.base = normalizeBase(base) // start with a route object that stands for "nowhere" - this.current = START + this.current = [START] this.pending = null this.ready = false this.readyCbs = [] @@ -62,24 +65,31 @@ export class History { this.errorCbs.push(errorCb) } + getClosestCurrent (n: number) { + if (n >= this.current.length) { + return this.current[this.current.length - 1] + } + return this.current[n] + } + transitionTo ( - location: RawLocation, + locations: Array, onComplete?: Function, onAbort?: Function ) { - const route = this.router.match(location, this.current) + const routes = locations.map((location, i) => this.router.match(location, this.getClosestCurrent(i))) this.confirmTransition( - route, - () => { - this.updateRoute(route) - onComplete && onComplete(route) + routes, + (equalLayers) => { + this.updateCurrent(routes, equalLayers) + onComplete && onComplete(routes) this.ensureURL() // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { - cb(route) + cb(routes) }) } }, @@ -97,7 +107,7 @@ export class History { ) } - confirmTransition (route: Route, onComplete: Function, onAbort?: Function) { + confirmTransition (routes: Array, onComplete: Function, onAbort?: Function) { const current = this.current const abort = err => { // after merging https://github.com/vuejs/vue-router/pull/2771 we @@ -116,18 +126,27 @@ export class History { } onAbort && onAbort(err) } - if ( - isSameRoute(route, current) && - // in the case the route map has been dynamically appended to - route.matched.length === current.matched.length - ) { + + let equalLayers = 0 + for (equalLayers = 0; equalLayers < routes.length && equalLayers < current.length; equalLayers++) { + if ( + !isSameRoute(routes[equalLayers], current[equalLayers]) || + // in the case the route map has been dynamically appended to + routes[equalLayers].matched.length !== current[equalLayers].matched.length + ) { + break + } + } + + if (equalLayers === routes.length && equalLayers === current.length) { this.ensureURL() - return abort(new NavigationDuplicated(route)) + return abort(new NavigationDuplicated(routes)) } - const { updated, deactivated, activated } = resolveQueue( - this.current.matched, - route.matched + const { updated, deactivated, activated } = resolveQueues( + current, + routes, + equalLayers ) const queue: Array = [].concat( @@ -143,13 +162,13 @@ export class History { resolveAsyncComponents(activated) ) - this.pending = route + this.pending = routes const iterator = (hook: NavigationGuard, next) => { - if (this.pending !== route) { + if (this.pending !== routes) { return abort() } try { - hook(route, current, (to: any) => { + hook(routes, current, (to: any) => { if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL this.ensureURL(true) @@ -178,17 +197,17 @@ export class History { runQueue(queue, iterator, () => { const postEnterCbs = [] - const isValid = () => this.current === route + const isValid = () => this.current === routes // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { - if (this.pending !== route) { + if (this.pending !== routes) { return abort() } this.pending = null - onComplete(route) + onComplete(equalLayers) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { @@ -200,12 +219,12 @@ export class History { }) } - updateRoute (route: Route) { + updateCurrent (routes: Array, equalLayers: number) { const prev = this.current - this.current = route - this.cb && this.cb(route) + this.current = routes + this.cb && this.cb(routes, equalLayers) this.router.afterHooks.forEach(hook => { - hook && hook(route, prev) + hook && hook(routes, prev) }) } } @@ -230,6 +249,37 @@ function normalizeBase (base: ?string): string { return base.replace(/\/$/, '') } +function resolveQueues ( + current: Array, + next: Array, + equalLayers: number +): { + updated: Array, + activated: Array, + deactivated: Array +} { + const res = { + updated: [], + activated: [], + deactivated: [] + } + + const min = Math.min(current.length, next.length) + for (let i = equalLayers; i < min; i++) { + const r = resolveQueue(current[i].matched, next[i].matched) + res.updated.push(...r.updated) + res.activated.push(...r.activated) + res.deactivated.push(...r.deactivated) + } + for (let i = min; i < current.length; i++) { + res.deactivated.push(...current[i].matched) + } + for (let i = min; i < next.length; i++) { + res.activated.push(...next[i].matched) + } + return res +} + function resolveQueue ( current: Array, next: Array diff --git a/src/history/html5.js b/src/history/html5.js index e1cdba97d..3983d0786 100644 --- a/src/history/html5.js +++ b/src/history/html5.js @@ -5,7 +5,7 @@ import { History } from './base' import { cleanPath } from '../util/path' import { START } from '../util/route' import { setupScroll, handleScroll } from '../util/scroll' -import { pushState, replaceState, supportsPushState } from '../util/push-state' +import { pushState, supportsPushState } from '../util/push-state' export class HTML5History extends History { constructor (router: Router, base: ?string) { @@ -29,7 +29,11 @@ export class HTML5History extends History { return } - this.transitionTo(location, route => { + let locations = [location] + if (window.history.state.state) { + locations = window.history.state.state + } + this.transitionTo(locations, route => { if (supportsScroll) { handleScroll(router, route, current, true) } @@ -41,28 +45,51 @@ export class HTML5History extends History { window.history.go(n) } - push (location: RawLocation, onComplete?: Function, onAbort?: Function) { + navigateAllLayers (locations: Array, push: boolean, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this - this.transitionTo(location, route => { - pushState(cleanPath(this.base + route.fullPath)) + this.transitionTo(locations, routes => { + this.ensureURL(push) + const route = this.current[this.current.length - 1] handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } - replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { - const { current: fromRoute } = this - this.transitionTo(location, route => { - replaceState(cleanPath(this.base + route.fullPath)) - handleScroll(this.router, route, fromRoute, false) - onComplete && onComplete(route) - }, onAbort) + navigateLastLayer (location: RawLocation, push: boolean, onComplete?: Function, onAbort?: Function) { + const locations = [ + ...this.current.slice(0, -1).map(r => r.fullPath), + location + ] + this.navigateAllLayers(locations, push, onComplete, onAbort) + } + + navigateLayer (layer: number, location: RawLocation, push: boolean, onComplete?: Function, onAbort?: Function) { + const locations = [ + ...this.current.slice(0, layer).map(r => r.fullPath), + location, + ...this.current.slice(layer + 1).map(r => r.fullPath) + ] + this.navigateAllLayers(locations, push, onComplete, onAbort) + } + + navigateAddLayer (location: RawLocation, push: boolean, onComplete?: Function, onAbort?: Function) { + const locations = [ + ...this.current.map(r => r.fullPath), + location + ] + this.navigateAllLayers(locations, push, onComplete, onAbort) + } + + navigateRemoveLayer (location: RawLocation, push: boolean, onComplete?: Function, onAbort?: Function) { + const locations = this.current.slice(0, -1).map(r => r.fullPath) + this.navigateAllLayers(locations, push, onComplete, onAbort) } ensureURL (push?: boolean) { - if (getLocation(this.base) !== this.current.fullPath) { - const current = cleanPath(this.base + this.current.fullPath) - push ? pushState(current) : replaceState(current) + const route = this.current[this.current.length - 1] + if (getLocation(this.base) !== route.fullPath) { + const path = cleanPath(this.base + route.fullPath) + pushState(path, this.current.map(r => r.fullPath), !push) } } diff --git a/src/index.js b/src/index.js index b3eb25dd3..7b2ad7694 100644 --- a/src/index.js +++ b/src/index.js @@ -111,7 +111,7 @@ export default class VueRouter { const history = this.history if (history instanceof HTML5History) { - history.transitionTo(history.getCurrentLocation()) + history.transitionTo([history.getCurrentLocation()]) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() @@ -123,9 +123,21 @@ export default class VueRouter { ) } - history.listen(route => { + history.listen((routes, equalLayers) => { this.apps.forEach((app) => { - app._route = route + // Mutate only the changed routes, so we don't trigger unnecessary updates. + // Changed elements + for (let i = equalLayers; i < app._routes.length && i < routes.length; i++) { + app.$set(app._routes, i, routes[i]) + } + // Added elements + for (let i = app._routes.length; i < routes.length; i++) { + app._routes.push(routes[i]) + } + // Removed + while (app._routes.length > routes.length) { + app._routes.pop() + } }) }) } @@ -151,11 +163,43 @@ export default class VueRouter { } push (location: RawLocation, onComplete?: Function, onAbort?: Function) { - this.history.push(location, onComplete, onAbort) + this.history.navigateLastLayer(location, true, onComplete, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { - this.history.replace(location, onComplete, onAbort) + this.history.navigateLastLayer(location, false, onComplete, onAbort) + } + + pushAddLayer (location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.history.navigateAddLayer(location, true, onComplete, onAbort) + } + + replaceAddLayer (location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.history.navigateAddLayer(location, false, onComplete, onAbort) + } + + pushRemoveLayer (onComplete?: Function, onAbort?: Function) { + this.history.navigateRemoveLayer(true, onComplete, onAbort) + } + + replaceRemoveLayer (onComplete?: Function, onAbort?: Function) { + this.history.navigateRemoveLayer(false, onComplete, onAbort) + } + + pushLayer (layer: number, location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.history.navigateLayer(layer, location, true, onComplete, onAbort) + } + + replaceLayer (layer: number, location: RawLocation, onComplete?: Function, onAbort?: Function) { + this.history.navigateLayer(layer, location, false, onComplete, onAbort) + } + + pushAllLayers (locations: Array, onComplete?: Function, onAbort?: Function) { + this.history.navigateAllLayers(locations, true, onComplete, onAbort) + } + + replaceAllLayers (locations: Array, onComplete?: Function, onAbort?: Function) { + this.history.navigateAllLayers(locations, false, onComplete, onAbort) } go (n: number) { diff --git a/src/install.js b/src/install.js index ee8506aa8..9149f4ca1 100644 --- a/src/install.js +++ b/src/install.js @@ -2,6 +2,20 @@ import View from './components/view' import Link from './components/link' export let _Vue +class VueRouterLayer { + constructor (router, layer) { + this.router = router + this.layer = layer + } + + push (location, onComplete, onAbort) { + return this.router.pushLayer(this.layer, location, onComplete, onAbort) + } + + replace (location, onComplete, onAbort) { + return this.router.replaceLayer(this.layer, location, onComplete, onAbort) + } +} export function install (Vue) { if (install.installed && _Vue === Vue) return @@ -24,9 +38,11 @@ export function install (Vue) { this._routerRoot = this this._router = this.$options.router this._router.init(this) - Vue.util.defineReactive(this, '_route', this._router.history.current) + this._routerLayer = 0 + Vue.util.defineReactive(this, '_routes', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this + this._routerLayer = this.$parent ? this.$parent._routerLayer : 0 } registerInstance(this, this) }, @@ -39,8 +55,12 @@ export function install (Vue) { get () { return this._routerRoot._router } }) + Object.defineProperty(Vue.prototype, '$routerLayer', { + get () { return new VueRouterLayer(this._routerRoot._router, this._routerLayer) } + }) + Object.defineProperty(Vue.prototype, '$route', { - get () { return this._routerRoot._route } + get () { return this._routerRoot._routes[this._routerLayer] } }) Vue.component('RouterView', View) diff --git a/src/util/push-state.js b/src/util/push-state.js index 136d6f9ec..47e399e8d 100644 --- a/src/util/push-state.js +++ b/src/util/push-state.js @@ -37,17 +37,17 @@ export function setStateKey (key: string) { _key = key } -export function pushState (url?: string, replace?: boolean) { +export function pushState (url?: string, state: any, replace?: boolean) { saveScrollPosition() // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history try { if (replace) { - history.replaceState({ key: _key }, '', url) + history.replaceState({ key: _key, state }, '', url) } else { _key = genKey() - history.pushState({ key: _key }, '', url) + history.pushState({ key: _key, state }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url)