/* @flow */
|
|
import { createRoute, isSameRoute, isIncludedRoute } from '../util/route'
|
import { extend } from '../util/misc'
|
import { normalizeLocation } from '../util/location'
|
import { warn } from '../util/warn'
|
|
// work around weird flow bug
|
const toTypes: Array<Function> = [String, Object]
|
const eventTypes: Array<Function> = [String, Array]
|
|
const noop = () => {}
|
|
let warnedCustomSlot
|
let warnedTagProp
|
let warnedEventProp
|
|
export default {
|
name: 'RouterLink',
|
props: {
|
to: {
|
type: toTypes,
|
required: true
|
},
|
tag: {
|
type: String,
|
default: 'a'
|
},
|
custom: Boolean,
|
exact: Boolean,
|
exactPath: Boolean,
|
append: Boolean,
|
replace: Boolean,
|
activeClass: String,
|
exactActiveClass: String,
|
ariaCurrentValue: {
|
type: String,
|
default: 'page'
|
},
|
event: {
|
type: eventTypes,
|
default: 'click'
|
}
|
},
|
render (h: Function) {
|
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, this.exactPath)
|
classes[activeClass] = this.exact || this.exactPath
|
? classes[exactActiveClass]
|
: isIncludedRoute(current, compareTarget)
|
|
const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
|
|
const handler = e => {
|
if (guardEvent(e)) {
|
if (this.replace) {
|
router.replace(location, noop)
|
} else {
|
router.push(location, noop)
|
}
|
}
|
}
|
|
const on = { click: guardEvent }
|
if (Array.isArray(this.event)) {
|
this.event.forEach(e => {
|
on[e] = handler
|
})
|
} else {
|
on[this.event] = handler
|
}
|
|
const data: any = { class: classes }
|
|
const scopedSlot =
|
!this.$scopedSlots.$hasNormal &&
|
this.$scopedSlots.default &&
|
this.$scopedSlots.default({
|
href,
|
route,
|
navigate: handler,
|
isActive: classes[activeClass],
|
isExactActive: classes[exactActiveClass]
|
})
|
|
if (scopedSlot) {
|
if (process.env.NODE_ENV !== 'production' && !this.custom) {
|
!warnedCustomSlot && warn(false, 'In Vue Router 4, the v-slot API will by default wrap its content with an <a> element. Use the custom prop to remove this warning:\n<router-link v-slot="{ navigate, href }" custom></router-link>\n')
|
warnedCustomSlot = true
|
}
|
if (scopedSlot.length === 1) {
|
return scopedSlot[0]
|
} else if (scopedSlot.length > 1 || !scopedSlot.length) {
|
if (process.env.NODE_ENV !== 'production') {
|
warn(
|
false,
|
`<router-link> with to="${
|
this.to
|
}" is trying to use a scoped slot but it didn't provide exactly one child. Wrapping the content with a span element.`
|
)
|
}
|
return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
|
}
|
}
|
|
if (process.env.NODE_ENV !== 'production') {
|
if ('tag' in this.$options.propsData && !warnedTagProp) {
|
warn(
|
false,
|
`<router-link>'s tag prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
|
)
|
warnedTagProp = true
|
}
|
if ('event' in this.$options.propsData && !warnedEventProp) {
|
warn(
|
false,
|
`<router-link>'s event prop is deprecated and has been removed in Vue Router 4. Use the v-slot API to remove this warning: https://next.router.vuejs.org/guide/migration/#removal-of-event-and-tag-props-in-router-link.`
|
)
|
warnedEventProp = true
|
}
|
}
|
|
if (this.tag === 'a') {
|
data.on = on
|
data.attrs = { href, 'aria-current': ariaCurrentValue }
|
} else {
|
// find the first <a> child and apply listener and href
|
const a = findAnchor(this.$slots.default)
|
if (a) {
|
// in case the <a> is a static node
|
a.isStatic = false
|
const aData = (a.data = extend({}, a.data))
|
aData.on = aData.on || {}
|
// transform existing events in both objects into arrays so we can push later
|
for (const event in aData.on) {
|
const handler = aData.on[event]
|
if (event in on) {
|
aData.on[event] = Array.isArray(handler) ? handler : [handler]
|
}
|
}
|
// append new listeners for router-link
|
for (const event in on) {
|
if (event in aData.on) {
|
// on[event] is always a function
|
aData.on[event].push(on[event])
|
} else {
|
aData.on[event] = handler
|
}
|
}
|
|
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
|
aAttrs.href = href
|
aAttrs['aria-current'] = ariaCurrentValue
|
} else {
|
// doesn't have <a> 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
|
}
|
}
|
}
|
}
|