import { extend } from '../../vue' import { NAME_LINK } from '../../constants/components' import { EVENT_NAME_CLICK } from '../../constants/events' import { PROP_TYPE_ARRAY_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_OBJECT_STRING, PROP_TYPE_STRING } from '../../constants/props' import { concat } from '../../utils/array' import { attemptBlur, attemptFocus, isTag } from '../../utils/dom' import { getRootEventName, stopEvent } from '../../utils/events' import { isBoolean, isEvent, isFunction, isUndefined } from '../../utils/inspect' import { omit, sortKeys } from '../../utils/object' import { makeProp, makePropsConfigurable, pluckProps } from '../../utils/props' import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router' import { attrsMixin } from '../../mixins/attrs' import { listenOnRootMixin } from '../../mixins/listen-on-root' import { listenersMixin } from '../../mixins/listeners' import { normalizeSlotMixin } from '../../mixins/normalize-slot' // --- Constants --- const ROOT_EVENT_NAME_CLICKED = getRootEventName(NAME_LINK, 'clicked') // --- Props --- // `` specific props export const routerLinkProps = { activeClass: makeProp(PROP_TYPE_STRING), append: makeProp(PROP_TYPE_BOOLEAN, false), event: makeProp(PROP_TYPE_ARRAY_STRING), exact: makeProp(PROP_TYPE_BOOLEAN, false), exactActiveClass: makeProp(PROP_TYPE_STRING), exactPath: makeProp(PROP_TYPE_BOOLEAN, false), exactPathActiveClass: makeProp(PROP_TYPE_STRING), replace: makeProp(PROP_TYPE_BOOLEAN, false), routerTag: makeProp(PROP_TYPE_STRING), to: makeProp(PROP_TYPE_OBJECT_STRING) } // `` specific props export const nuxtLinkProps = { noPrefetch: makeProp(PROP_TYPE_BOOLEAN, false), // Must be `null` to fall back to the value defined in the // `nuxt.config.js` configuration file for `router.prefetchLinks` // We convert `null` to `undefined`, so that Nuxt.js will use the // compiled default // Vue treats `undefined` as default of `false` for Boolean props, // so we must set it as `null` here to be a true tri-state prop prefetch: makeProp(PROP_TYPE_BOOLEAN, null) } // All `` props export const props = makePropsConfigurable( sortKeys({ ...nuxtLinkProps, ...routerLinkProps, active: makeProp(PROP_TYPE_BOOLEAN, false), disabled: makeProp(PROP_TYPE_BOOLEAN, false), href: makeProp(PROP_TYPE_STRING), // Must be `null` if no value provided rel: makeProp(PROP_TYPE_STRING, null), // To support 3rd party router links based on `` (i.e. `g-link` for Gridsome) // Default is to auto choose between `` and `` // Gridsome doesn't provide a mechanism to auto detect and has caveats // such as not supporting FQDN URLs or hash only URLs routerComponentName: makeProp(PROP_TYPE_STRING), target: makeProp(PROP_TYPE_STRING, '_self') }), NAME_LINK ) // --- Main component --- // @vue/component export const BLink = /*#__PURE__*/ extend({ name: NAME_LINK, // Mixin order is important! mixins: [attrsMixin, listenersMixin, listenOnRootMixin, normalizeSlotMixin], inheritAttrs: false, props, computed: { computedTag() { // We don't pass `this` as the first arg as we need reactivity of the props const { to, disabled, routerComponentName } = this return computeTag({ to, disabled, routerComponentName }, this) }, isRouterLink() { return isRouterLink(this.computedTag) }, computedRel() { // We don't pass `this` as the first arg as we need reactivity of the props const { target, rel } = this return computeRel({ target, rel }) }, computedHref() { // We don't pass `this` as the first arg as we need reactivity of the props const { to, href } = this return computeHref({ to, href }, this.computedTag) }, computedProps() { const { event, prefetch, routerTag } = this return this.isRouterLink ? { ...pluckProps( omit( { ...routerLinkProps, ...(this.computedTag === 'nuxt-link' ? nuxtLinkProps : {}) }, ['event', 'prefetch', 'routerTag'] ), this ), // Only add these props, when actually defined ...(event ? { event } : {}), ...(isBoolean(prefetch) ? { prefetch } : {}), // Pass `router-tag` as `tag` prop ...(routerTag ? { tag: routerTag } : {}) } : {} }, computedAttrs() { const { bvAttrs, computedHref: href, computedRel: rel, disabled, target, routerTag, isRouterLink } = this return { ...bvAttrs, // If `href` attribute exists on `` (even `undefined` or `null`) // it fails working on SSR, so we explicitly add it here if needed // (i.e. if `computedHref` is truthy) ...(href ? { href } : {}), // We don't render `rel` or `target` on non link tags when using `vue-router` ...(isRouterLink && routerTag && !isTag(routerTag, 'a') ? {} : { rel, target }), tabindex: disabled ? '-1' : isUndefined(bvAttrs.tabindex) ? null : bvAttrs.tabindex, 'aria-disabled': disabled ? 'true' : null } }, computedListeners() { return { // Transfer all listeners (native) to the root element ...this.bvListeners, // We want to overwrite any click handler since our callback // will invoke the user supplied handler(s) if `!this.disabled` click: this.onClick } } }, methods: { onClick(event) { const eventIsEvent = isEvent(event) const isRouterLink = this.isRouterLink const suppliedHandler = this.bvListeners.click if (eventIsEvent && this.disabled) { // Stop event from bubbling up // Kill the event loop attached to this specific `EventTarget` // Needed to prevent `vue-router` for doing its thing stopEvent(event, { immediatePropagation: true }) } else { // Router links do not emit instance `click` events, so we // add in an `$emit('click', event)` on its Vue instance // // seems not to be required for Vue3 compat build /* istanbul ignore next: difficult to test, but we know it works */ if (isRouterLink) { event.currentTarget.__vue__?.$emit(EVENT_NAME_CLICK, event) } // Call the suppliedHandler(s), if any provided concat(suppliedHandler) .filter(h => isFunction(h)) .forEach(handler => { handler(...arguments) }) // Emit the global `$root` click event this.emitOnRoot(ROOT_EVENT_NAME_CLICKED, event) // TODO: Remove deprecated 'clicked::link' event with next major release this.emitOnRoot('clicked::link', event) } // Stop scroll-to-top behavior or navigation on // regular links when href is just '#' if (eventIsEvent && !isRouterLink && this.computedHref === '#') { stopEvent(event, { propagation: false }) } }, focus() { attemptFocus(this.$el) }, blur() { attemptBlur(this.$el) } }, render(h) { const { active, disabled } = this return h( this.computedTag, { class: { active, disabled }, attrs: this.computedAttrs, props: this.computedProps, // We must use `nativeOn` for ``/`` instead of `on` [this.isRouterLink ? 'nativeOn' : 'on']: this.computedListeners }, this.normalizeSlot() ) } })