diff --git a/docs/common-props.json b/docs/common-props.json index 78ccfa2bf83..25cb814ad55 100644 --- a/docs/common-props.json +++ b/docs/common-props.json @@ -197,44 +197,49 @@ "active": { "description": "When set to 'true', places the component in the active state with active styling" }, + "href": { + "description": " prop: Denotes the target URL of the link for standard a links" + }, "rel": { - "description": "Sets the 'rel' attribute on the rendered link" + "description": " prop: Sets the 'rel' attribute on the rendered link" }, "target": { - "description": "Sets the 'target' attribute on the rendered link" - }, - "href": { - "description": "Denotes the target URL of the link for standard a links" + "description": " prop: Sets the 'target' attribute on the rendered link" }, "to": { - "description": "router-link prop: Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object" + "description": " prop: Denotes the target route of the link. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object" }, "replace": { - "description": "router-link prop: Setting the replace prop will call 'router.replace()' instead of 'router.push()' when clicked, so the navigation will not leave a history record" + "description": " prop: Setting the replace prop will call 'router.replace()' instead of 'router.push()' when clicked, so the navigation will not leave a history record" }, "append": { - "description": "router-link prop: Setting append prop always appends the relative path to the current path" + "description": " prop: Setting append prop always appends the relative path to the current path" }, "exact": { - "description": "router-link prop: The default active class matching behavior is inclusive match. Setting this prop forces the mode to exactly match the route" + "description": " prop: The default active class matching behavior is inclusive match. Setting this prop forces the mode to exactly match the route" }, "activeClass": { - "description": "router-link prop: Configure the active CSS class applied when the link is active. Typically you will want to set this to class name 'active'" + "description": " prop: Configure the active CSS class applied when the link is active. Typically you will want to set this to class name 'active'" }, "exactActiveClass": { - "description": "router-link prop: Configure the active CSS class applied when the link is active with exact match. Typically you will want to set this to class name 'active'" + "description": " prop: Configure the active CSS class applied when the link is active with exact match. Typically you will want to set this to class name 'active'" }, "routerTag": { - "description": "router-link prop: Specify which tag to render, and it will still listen to click events for navigation. 'router-tag' translates to the tag prop on the final rendered router-link. Typically you should use the default value" + "description": " prop: Specify which tag to render, and it will still listen to click events for navigation. 'router-tag' translates to the tag prop on the final rendered router-link. Typically you should use the default value" }, "event": { - "description": "router-link prop: Specify the event that triggers the link. In most cases you should leave this as the default" + "description": " prop: Specify the event that triggers the link. In most cases you should leave this as the default" }, "prefetch": { - "description": "nuxt-link prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'prefetch' to 'true' or 'false' will overwrite the default value of 'router.prefetchLinks'", + "description": " prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'prefetch' to 'true' or 'false' will overwrite the default value of 'router.prefetchLinks'", "version": "2.15.0" }, "noPrefetch": { - "description": "nuxt-link prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'no-prefetch' will disabled this feature for the specific link" + "description": " prop: To improve the responsiveness of your Nuxt.js applications, when the link will be displayed within the viewport, Nuxt.js will automatically prefetch the code splitted page. Setting 'no-prefetch' will disabled this feature for the specific link" + }, + "routerComponentName": { + "description": " prop: BootstrapVue auto detects between `` and ``. In cases where you want to use a 3rd party link component based on ``, set this prop to the component name. e.g. set it to 'g-link' if you are using Gridsome (note only `` specific props are passed to the component)", + "version": "2.15.0", + "settings": true } } diff --git a/docs/markdown/reference/router-links/README.md b/docs/markdown/reference/router-links/README.md index 796cd12ee93..88e7a91de36 100644 --- a/docs/markdown/reference/router-links/README.md +++ b/docs/markdown/reference/router-links/README.md @@ -10,7 +10,8 @@ In the following sections, we are using the `` component to render router links. `` is the building block of most of BootstrapVue's _actionable_ components. You could use any other component that supports link generation such as [``](/docs/components/link), -[``](/docs/components/button), [``](/docs/components/breadcrumb), +[``](/docs/components/button), [``](/docs/components/avatar), +[``](/docs/components/breadcrumb), [``](/docs/components/list-group), [``](/docs/components/nav), [``](/docs/components/dropdown), and [``](/docs/components/pagination-nav). Note that not all props are available on @@ -203,3 +204,32 @@ disabled this feature for the specific link. **Note:** If you have prefetching disabled in your `nuxt.config.js` configuration (`router: { prefetchLinks: false }`), or are using a version of Nuxt.js `< 2.4.0`, then this prop will have no effect. + +## Third-party router link support + +v2.15.0+ + +BootstrapVue auto detects using `` and `` link components. Some 3rd party +frameworks also provide customized versions of ``, such as +[Gridsome's `` component](https://gridsome.org/docs/linking/). BootstrapVue can support +these third party `` compatible components via the use of the `router-component-name` +prop. All `vue-router` props (excluding `` specific props) will be passed to the +specified router link component. + +**Notes:** + +- The 3rd party component will only be used when the `to` prop is set. +- Not all 3rd party components support all props supported by ``, nor do not support + fully qualified domain name URLs, nor hash only URLs. Refer to the 3rd party component + documentation for details. + +### `router-component-name` + +- type: `string` +- default: `undefined` +- availability: BootstrapVue 2.15.0+ + +Set this prop to the name of the `` compatible component, e.g. `'g-link'` for +[Gridsome](https://gridsome.org/). + +If left at the default, BootstrapVue will automatically select `` or ``. diff --git a/src/components/avatar/avatar.js b/src/components/avatar/avatar.js index 6501aede88e..d9974a53b6a 100644 --- a/src/components/avatar/avatar.js +++ b/src/components/avatar/avatar.js @@ -3,6 +3,8 @@ import pluckProps from '../../utils/pluck-props' import { getComponentConfig } from '../../utils/config' import { isNumber, isString, isUndefinedOrNull } from '../../utils/inspect' import { toFloat } from '../../utils/number' +import { omit } from '../../utils/object' +import { isLink } from '../../utils/router' import { BButton } from '../button/button' import { BLink, props as BLinkProps } from '../link/link' import { BIcon } from '../../icons/icon' @@ -25,23 +27,7 @@ const DEFAULT_SIZES = { } // --- Props --- -const linkProps = pluckProps( - [ - 'href', - 'rel', - 'target', - 'disabled', - 'to', - 'append', - 'replace', - 'activeClass', - 'exact', - 'exactActiveClass', - 'prefetch', - 'noPrefetch' - ], - BLinkProps -) +const linkProps = omit(BLinkProps, ['active', 'event', 'routerTag']) const props = { src: { @@ -208,14 +194,14 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({ fontStyle, marginStyle, computedSize: size, - button: isButton, + button, buttonType: type, badge, badgeVariant, badgeStyle } = this - const isBLink = !isButton && (this.href || this.to) - const tag = isButton ? BButton : isBLink ? BLink : 'span' + const link = !button && isLink(this) + const tag = button ? BButton : link ? BLink : 'span' const alt = this.alt || null const ariaLabel = this.ariaLabel || null @@ -261,7 +247,7 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({ staticClass: CLASS_NAME, class: { // We use badge styles for theme variants when not rendering `BButton` - [`badge-${variant}`]: !isButton && variant, + [`badge-${variant}`]: !button && variant, // Rounding/Square rounded: rounded === true, [`rounded-${rounded}`]: rounded && rounded !== true, @@ -270,8 +256,8 @@ export const BAvatar = /*#__PURE__*/ Vue.extend({ }, style: { width: size, height: size, ...marginStyle }, attrs: { 'aria-label': ariaLabel || null }, - props: isButton ? { variant, disabled, type } : isBLink ? pluckProps(linkProps, this) : {}, - on: isBLink || isButton ? { click: this.onClick } : {} + props: button ? { variant, disabled, type } : link ? pluckProps(linkProps, this) : {}, + on: button || link ? { click: this.onClick } : {} } return h(tag, componentData, [$content, $badge]) diff --git a/src/components/badge/badge.js b/src/components/badge/badge.js index ab2399e16fe..47941d8e2ba 100644 --- a/src/components/badge/badge.js +++ b/src/components/badge/badge.js @@ -2,12 +2,17 @@ import Vue from '../../utils/vue' import pluckProps from '../../utils/pluck-props' import { mergeData } from 'vue-functional-data-merge' import { getComponentConfig } from '../../utils/config' -import { clone } from '../../utils/object' +import { omit } from '../../utils/object' +import { isLink } from '../../utils/router' import { BLink, props as BLinkProps } from '../link/link' +// --- Constants --- + const NAME = 'BBadge' -const linkProps = clone(BLinkProps) +// --- Props --- + +const linkProps = omit(BLinkProps, ['event', 'routerTag']) delete linkProps.href.default delete linkProps.to.default @@ -27,14 +32,15 @@ export const props = { ...linkProps } +// --- Main component --- // @vue/component export const BBadge = /*#__PURE__*/ Vue.extend({ name: NAME, functional: true, props, render(h, { props, data, children }) { - const isBLink = props.href || props.to - const tag = isBLink ? BLink : props.tag + const link = isLink(props) + const tag = link ? BLink : props.tag const componentData = { staticClass: 'badge', @@ -46,7 +52,7 @@ export const BBadge = /*#__PURE__*/ Vue.extend({ disabled: props.disabled } ], - props: isBLink ? pluckProps(linkProps, props) : {} + props: link ? pluckProps(linkProps, props) : {} } return h(tag, mergeData(data, componentData), children) diff --git a/src/components/breadcrumb/breadcrumb-link.js b/src/components/breadcrumb/breadcrumb-link.js index a2cc8b1961e..232d1f99d85 100644 --- a/src/components/breadcrumb/breadcrumb-link.js +++ b/src/components/breadcrumb/breadcrumb-link.js @@ -1,7 +1,8 @@ -import Vue from '../../utils/vue' import { mergeData } from 'vue-functional-data-merge' +import Vue from '../../utils/vue' import pluckProps from '../../utils/pluck-props' import { htmlOrText } from '../../utils/html' +import { omit } from '../../utils/object' import { BLink, props as BLinkProps } from '../link/link' export const props = { @@ -17,7 +18,7 @@ export const props = { type: String, default: 'location' }, - ...BLinkProps + ...omit(BLinkProps, ['event', 'routerTag']) } // @vue/component diff --git a/src/components/button/button.js b/src/components/button/button.js index e7c9382c556..7704e66bfbf 100644 --- a/src/components/button/button.js +++ b/src/components/button/button.js @@ -4,10 +4,10 @@ import KeyCodes from '../../utils/key-codes' import pluckProps from '../../utils/pluck-props' import { concat } from '../../utils/array' import { getComponentConfig } from '../../utils/config' -import { addClass, removeClass } from '../../utils/dom' +import { addClass, isTag, removeClass } from '../../utils/dom' import { isBoolean, isEvent, isFunction } from '../../utils/inspect' -import { clone } from '../../utils/object' -import { toString } from '../../utils/string' +import { omit } from '../../utils/object' +import { isLink as isLinkStrict } from '../../utils/router' import { BLink, props as BLinkProps } from '../link/link' // --- Constants --- @@ -16,7 +16,7 @@ const NAME = 'BButton' // --- Props --- -const linkProps = clone(BLinkProps) +const linkProps = omit(BLinkProps, ['event', 'routerTag']) delete linkProps.href.default delete linkProps.to.default @@ -65,9 +65,6 @@ export const props = { ...btnProps, ...linkProps } // --- Helper methods --- -// Returns `true` if a tag's name equals `name` -const tagIs = (tag, name) => toString(tag).toLowerCase() === toString(name).toLowerCase() - // Focus handler for toggle buttons // Needs class of 'focus' when focused const handleFocus = evt => { @@ -80,13 +77,13 @@ const handleFocus = evt => { // Is the requested button a link? // If tag prop is set to `a`, we use a to get proper disabled handling -const isLink = props => props.href || props.to || tagIs(props.tag, 'a') +const isLink = props => isLinkStrict(props) || isTag(props.tag, 'a') // Is the button to be a toggle button? const isToggle = props => isBoolean(props.pressed) // Is the button "really" a button? -const isButton = props => !(isLink(props) || (props.tag && !tagIs(props.tag, 'button'))) +const isButton = props => !(isLink(props) || (props.tag && !isTag(props.tag, 'button'))) // Is the requested tag not a button or link? const isNonStandardTag = props => !isLink(props) && !isButton(props) @@ -105,7 +102,7 @@ const computeClass = props => [ ] // Compute the link props to pass to b-link (if required) -const computeLinkProps = props => (isLink(props) ? pluckProps(linkProps, props) : null) +const computeLinkProps = props => (isLink(props) ? pluckProps(linkProps, props) : {}) // Compute the attributes for a button const computeAttrs = (props, data) => { diff --git a/src/components/dropdown/dropdown-item.js b/src/components/dropdown/dropdown-item.js index 01af4fe1478..d4c2abacd07 100644 --- a/src/components/dropdown/dropdown-item.js +++ b/src/components/dropdown/dropdown-item.js @@ -1,11 +1,11 @@ import Vue from '../../utils/vue' import { requestAF } from '../../utils/dom' -import { clone } from '../../utils/object' +import { omit } from '../../utils/object' import attrsMixin from '../../mixins/attrs' import normalizeSlotMixin from '../../mixins/normalize-slot' import { BLink, props as BLinkProps } from '../link/link' -export const props = clone(BLinkProps) +export const props = omit(BLinkProps, ['event', 'routerTag']) // @vue/component export const BDropdownItem = /*#__PURE__*/ Vue.extend({ diff --git a/src/components/dropdown/package.json b/src/components/dropdown/package.json index a049dd7a235..4c541db9974 100644 --- a/src/components/dropdown/package.json +++ b/src/components/dropdown/package.json @@ -97,7 +97,7 @@ }, { "prop": "splitTo", - "description": "router-link prop: Denotes the target route of the split button. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object" + "description": " prop: Denotes the target route of the split button. When clicked, the value of the to prop will be passed to router.push() internally, so the value can be either a string or a Location descriptor object" }, { "prop": "splitVariant", diff --git a/src/components/link/README.md b/src/components/link/README.md index 2a381eabbd8..d51b06f6f71 100644 --- a/src/components/link/README.md +++ b/src/components/link/README.md @@ -26,6 +26,17 @@ If your app is running under [Nuxt.js](https://nuxtjs.org), the ``. The `` component supports all the same features as `` (as it is a wrapper component for ``) and more. +### Third party rounter links + +BootstrapVue auto detects using `` and `` link components. Some 3rd party +frameworks also provide customized versions of ``, such as +[Gridsome's `` component](https://gridsome.org/docs/linking/). `` can support these +third party `` compatible components via the use of the `router-component-name` prop. +All `vue-router` props (excluding `` specific props) will be passed to the specified +router link component. + +Note that the 3rd party component will only be used when the `to` prop is set. + ## Links with `href="#"` Typically `` will cause the document to scroll to the top of page when clicked. diff --git a/src/components/link/link.js b/src/components/link/link.js index 539af3514c6..60e5c58036a 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -1,6 +1,7 @@ import Vue from '../../utils/vue' import pluckProps from '../../utils/pluck-props' import { concat } from '../../utils/array' +import { getComponentConfig } from '../../utils/config' import { attemptBlur, attemptFocus } from '../../utils/dom' import { isBoolean, isEvent, isFunction, isUndefined } from '../../utils/inspect' import { computeHref, computeRel, computeTag, isRouterLink } from '../../utils/router' @@ -8,6 +9,10 @@ import attrsMixin from '../../mixins/attrs' import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' +// --- Constants --- + +const NAME = 'BLink' + // --- Props --- // specific props @@ -87,7 +92,15 @@ export const props = { default: false }, ...routerLinkProps, - ...nuxtLinkProps + ...nuxtLinkProps, + // 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: { + type: String, + default: () => getComponentConfig(NAME, 'routerComponentName') + } } // --- Main component --- @@ -101,7 +114,8 @@ export const BLink = /*#__PURE__*/ Vue.extend({ computed: { computedTag() { // We don't pass `this` as the first arg as we need reactivity of the props - return computeTag({ to: this.to, disabled: this.disabled }, this) + const { to, disabled, routerComponentName } = this + return computeTag({ to, disabled, routerComponentName }, this) }, isRouterLink() { return isRouterLink(this.computedTag) @@ -118,7 +132,7 @@ export const BLink = /*#__PURE__*/ Vue.extend({ const prefetch = this.prefetch return this.isRouterLink ? { - ...pluckProps({ ...routerLinkProps, ...nuxtLinkProps }, this.$props), + ...pluckProps({ ...routerLinkProps, ...nuxtLinkProps }, this), // Coerce `prefetch` value `null` to be `undefined` prefetch: isBoolean(prefetch) ? prefetch : undefined, // Pass `router-tag` as `tag` prop diff --git a/src/components/link/link.spec.js b/src/components/link/link.spec.js index 7f3913abb5e..6f404cfc7b8 100644 --- a/src/components/link/link.spec.js +++ b/src/components/link/link.spec.js @@ -326,6 +326,24 @@ describe('b-link', () => { ] }) + // Fake Gridsome `` component + const GLink = { + name: 'GLink', + props: { + to: { + type: [String, Object], + default: '' + } + }, + render(h) { + // We just us a simple A tag to render the + // fake `` and assume `to` is a string + return h('a', { attrs: { href: this.to } }, [this.$slots.default]) + } + } + + localVue.component('GLink', GLink) + const App = { router, components: { BLink }, @@ -339,6 +357,8 @@ describe('b-link', () => { h('b-link', { props: { to: { path: '/b' } } }, ['to-path-b']), // regular link h('b-link', { props: { href: '/b' } }, ['href-a']), + // g-link + h('b-link', { props: { routerComponentName: 'g-link', to: '/a' } }, ['g-link-a']), h('router-view') ]) } @@ -352,7 +372,7 @@ describe('b-link', () => { expect(wrapper.vm).toBeDefined() expect(wrapper.element.tagName).toBe('MAIN') - expect(wrapper.findAll('a').length).toBe(4) + expect(wrapper.findAll('a').length).toBe(5) const $links = wrapper.findAll('a') @@ -374,6 +394,11 @@ describe('b-link', () => { expect($links.at(3).vm.$options.name).toBe('BLink') expect($links.at(3).vm.$children.length).toBe(0) + expect($links.at(4).vm).toBeDefined() + expect($links.at(4).vm.$options.name).toBe('BLink') + expect($links.at(4).vm.$children.length).toBe(1) + expect($links.at(4).vm.$children[0].$options.name).toBe('GLink') + wrapper.destroy() }) }) diff --git a/src/components/link/package.json b/src/components/link/package.json index 4671251ef80..b5400530c42 100644 --- a/src/components/link/package.json +++ b/src/components/link/package.json @@ -7,7 +7,20 @@ "components": [ { "component": "BLink", - "props": [], + "props": [ + { + "prop": "href", + "description": "Denotes the target URL of the link for standard a links" + }, + { + "prop": "rel", + "description": "Sets the 'rel' attribute on the rendered link" + }, + { + "prop": "target", + "description": "Sets the 'target' attribute on the rendered link" + } + ], "events": [ { "event": "click", diff --git a/src/components/list-group/list-group-item.js b/src/components/list-group/list-group-item.js index f41bf0c3e77..dcfdb6fcae5 100644 --- a/src/components/list-group/list-group-item.js +++ b/src/components/list-group/list-group-item.js @@ -3,7 +3,9 @@ import Vue from '../../utils/vue' import pluckProps from '../../utils/pluck-props' import { arrayIncludes } from '../../utils/array' import { getComponentConfig } from '../../utils/config' -import { clone } from '../../utils/object' +import { isTag } from '../../utils/dom' +import { omit } from '../../utils/object' +import { isLink } from '../../utils/router' import { BLink, props as BLinkProps } from '../link/link' // --- Constants --- @@ -14,7 +16,7 @@ const actionTags = ['a', 'router-link', 'button', 'b-link'] // --- Props --- -const linkProps = clone(BLinkProps) +const linkProps = omit(BLinkProps, ['event', 'routerTag']) delete linkProps.href.default delete linkProps.to.default @@ -45,13 +47,14 @@ export const BListGroupItem = /*#__PURE__*/ Vue.extend({ functional: true, props, render(h, { props, data, children }) { - const tag = props.button ? 'button' : !props.href && !props.to ? props.tag : BLink - const isAction = Boolean( - props.href || props.to || props.action || props.button || arrayIncludes(actionTags, props.tag) - ) + const { button, variant, active, disabled } = props + const link = isLink(props) + const tag = button ? 'button' : !link ? props.tag : BLink + const action = !!(props.action || link || button || arrayIncludes(actionTags, props.tag)) + const attrs = {} let itemProps = {} - if (tag === 'button') { + if (isTag(tag, 'button')) { if (!data.attrs || !data.attrs.type) { // Add a type for button is one not provided in passed attributes attrs.type = 'button' @@ -63,18 +66,21 @@ export const BListGroupItem = /*#__PURE__*/ Vue.extend({ } else { itemProps = pluckProps(linkProps, props) } - const componentData = { - attrs, - props: itemProps, - staticClass: 'list-group-item', - class: { - [`list-group-item-${props.variant}`]: props.variant, - 'list-group-item-action': isAction, - active: props.active, - disabled: props.disabled - } - } - return h(tag, mergeData(data, componentData), children) + return h( + tag, + mergeData(data, { + attrs, + props: itemProps, + staticClass: 'list-group-item', + class: { + [`list-group-item-${variant}`]: variant, + 'list-group-item-action': action, + active, + disabled + } + }), + children + ) } }) diff --git a/src/components/nav/nav-item.js b/src/components/nav/nav-item.js index a5796140354..28af3ab36da 100644 --- a/src/components/nav/nav-item.js +++ b/src/components/nav/nav-item.js @@ -1,11 +1,11 @@ import { mergeData } from 'vue-functional-data-merge' import Vue from '../../utils/vue' -import { clone } from '../../utils/object' +import { omit } from '../../utils/object' import { BLink, props as BLinkProps } from '../link/link' // --- Props --- -export const props = clone(BLinkProps) +export const props = omit(BLinkProps, ['event', 'routerTag']) // --- Main component --- // @vue/component diff --git a/src/components/navbar/navbar-brand.js b/src/components/navbar/navbar-brand.js index 144f8e2e0ef..835b8801192 100644 --- a/src/components/navbar/navbar-brand.js +++ b/src/components/navbar/navbar-brand.js @@ -1,21 +1,24 @@ import { mergeData } from 'vue-functional-data-merge' import Vue from '../../utils/vue' import pluckProps from '../../utils/pluck-props' -import { clone } from '../../utils/object' +import { omit } from '../../utils/object' import { BLink, props as BLinkProps } from '../link/link' -const linkProps = clone(BLinkProps) +// --- Props --- + +const linkProps = omit(BLinkProps, ['event', 'routerTag']) linkProps.href.default = undefined linkProps.to.default = undefined export const props = { - ...linkProps, tag: { type: String, default: 'div' - } + }, + ...linkProps } +// --- Main component --- // @vue/component export const BNavbarBrand = /*#__PURE__*/ Vue.extend({ name: 'BNavbarBrand', diff --git a/src/components/navbar/navbar.js b/src/components/navbar/navbar.js index eed8ad22cee..5700e79c064 100644 --- a/src/components/navbar/navbar.js +++ b/src/components/navbar/navbar.js @@ -1,10 +1,15 @@ import Vue from '../../utils/vue' import { getComponentConfig, getBreakpoints } from '../../utils/config' +import { isTag } from '../../utils/dom' import { isString } from '../../utils/inspect' import normalizeSlotMixin from '../../mixins/normalize-slot' +// --- Constants --- + const NAME = 'BNavbar' +// --- Props --- + export const props = { tag: { type: String, @@ -35,6 +40,7 @@ export const props = { } } +// --- Main component --- // @vue/component export const BNavbar = /*#__PURE__*/ Vue.extend({ name: NAME, @@ -73,7 +79,7 @@ export const BNavbar = /*#__PURE__*/ Vue.extend({ this.breakpointClass ], attrs: { - role: this.tag === 'nav' ? null : 'navigation' + role: isTag(this.tag, 'nav') ? null : 'navigation' } }, [this.normalizeSlot('default')] diff --git a/src/components/pagination-nav/pagination-nav.js b/src/components/pagination-nav/pagination-nav.js index b3b2678f8b9..fbcccf45c87 100644 --- a/src/components/pagination-nav/pagination-nav.js +++ b/src/components/pagination-nav/pagination-nav.js @@ -7,16 +7,20 @@ import { isBrowser } from '../../utils/env' import { isArray, isUndefined, isFunction, isObject } from '../../utils/inspect' import { mathMax } from '../../utils/math' import { toInteger } from '../../utils/number' +import { omit } from '../../utils/object' import { computeHref, parseQuery } from '../../utils/router' import { toString } from '../../utils/string' import { warn } from '../../utils/warn' import paginationMixin from '../../mixins/pagination' import { props as BLinkProps } from '../link/link' +// --- Constants --- + const NAME = 'BPaginationNav' -// Sanitize the provided number of pages (converting to a number) -export const sanitizeNumberOfPages = value => mathMax(toInteger(value, 0), 1) +// --- Props --- + +const linkProps = omit(BLinkProps, ['event', 'routerTag']) const props = { size: { @@ -61,9 +65,15 @@ const props = { type: Boolean, default: false }, - ...pluckProps(['activeClass', 'exact', 'exactActiveClass', 'prefetch', 'noPrefetch'], BLinkProps) + ...linkProps } +// --- Utility methods --- + +// Sanitize the provided number of pages (converting to a number) +export const sanitizeNumberOfPages = value => mathMax(toInteger(value, 0), 1) + +// --- Main component --- // The render function is brought in via the pagination mixin // @vue/component export const BPaginationNav = /*#__PURE__*/ Vue.extend({ @@ -175,38 +185,13 @@ export const BPaginationNav = /*#__PURE__*/ Vue.extend({ return info.link }, linkProps(pageNum) { + const props = pluckProps(linkProps, this) const link = this.makeLink(pageNum) - const { - disabled, - exact, - activeClass, - exactActiveClass, - append, - replace, - prefetch, - noPrefetch - } = this - - const props = { - target: this.target || null, - rel: this.rel || null, - disabled, - // The following props are only used if `BLink` detects router - exact, - activeClass, - exactActiveClass, - append, - replace, - // specific prop - prefetch, - noPrefetch - } if (this.useRouter || isObject(link)) { props.to = link } else { props.href = link } - return props }, resolveLink(to = '') { diff --git a/src/components/table/td.js b/src/components/table/td.js index cb6ce671823..0fb91c764dd 100644 --- a/src/components/table/td.js +++ b/src/components/table/td.js @@ -1,4 +1,5 @@ import Vue from '../../utils/vue' +import { isTag } from '../../utils/dom' import { isUndefinedOrNull } from '../../utils/inspect' import { toInteger } from '../../utils/number' import { toString } from '../../utils/string' @@ -6,6 +7,8 @@ import attrsMixin from '../../mixins/attrs' import listenersMixin from '../../mixins/listeners' import normalizeSlotMixin from '../../mixins/normalize-slot' +// --- Utility methods --- + // Parse a rowspan or colspan into a digit (or `null` if < `1` ) const parseSpan = value => { value = toInteger(value, 0) @@ -42,6 +45,7 @@ export const props = { } } +// --- Main component --- // TODO: // In Bootstrap v5, we won't need "sniffing" as table element variants properly inherit // to the child elements, so this can be converted to a functional component @@ -160,7 +164,7 @@ export const BTd = /*#__PURE__*/ Vue.extend({ // Header or footer cells role = 'columnheader' scope = colspan > 0 ? 'colspan' : 'col' - } else if (this.tag === 'th') { + } else if (isTag(this.tag, 'th')) { // th's in tbody role = 'rowheader' scope = rowspan > 0 ? 'rowgroup' : 'row' diff --git a/src/components/toast/toast.js b/src/components/toast/toast.js index f3e40224606..3f62de144e5 100644 --- a/src/components/toast/toast.js +++ b/src/components/toast/toast.js @@ -1,12 +1,15 @@ -import Vue from '../../utils/vue' import { Portal, Wormhole } from 'portal-vue' import BVTransition from '../../utils/bv-transition' +import Vue from '../../utils/vue' +import pluckProps from '../../utils/pluck-props' import { BvEvent } from '../../utils/bv-event.class' import { getComponentConfig } from '../../utils/config' import { requestAF } from '../../utils/dom' import { EVENT_OPTIONS_NO_CAPTURE, eventOnOff } from '../../utils/events' import { mathMax } from '../../utils/math' import { toInteger } from '../../utils/number' +import { pick } from '../../utils/object' +import { isLink } from '../../utils/router' import attrsMixin from '../../mixins/attrs' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' @@ -14,7 +17,7 @@ import normalizeSlotMixin from '../../mixins/normalize-slot' import scopedStyleAttrsMixin from '../../mixins/scoped-style-attrs' import { BToaster } from './toaster' import { BButtonClose } from '../button/button-close' -import { BLink } from '../link/link' +import { BLink, props as BLinkProps } from '../link/link' // --- Constants --- @@ -24,6 +27,8 @@ const MIN_DURATION = 1000 // --- Props --- +const linkProps = pick(BLinkProps, ['href', 'to']) + export const props = { id: { // Even though the ID prop is provided by idMixin, we @@ -92,19 +97,12 @@ export const props = { type: [String, Object, Array], default: () => getComponentConfig(NAME, 'bodyClass') }, - href: { - type: String - // default: null - }, - to: { - type: [String, Object] - // default: null - }, static: { // Render the toast in place, rather than in a portal-target type: Boolean, default: false - } + }, + ...linkProps } // @vue/component @@ -385,14 +383,14 @@ export const BToast = /*#__PURE__*/ Vue.extend({ ) } // Toast body - const isLink = this.href || this.to + const link = isLink(this) const $body = h( - isLink ? BLink : 'div', + link ? BLink : 'div', { staticClass: 'toast-body', class: this.bodyClass, - props: isLink ? { to: this.to, href: this.href } : {}, - on: isLink ? { click: this.onLinkClick } : {} + props: link ? pluckProps(linkProps, this) : {}, + on: link ? { click: this.onLinkClick } : {} }, [this.normalizeSlot('default', this.slotScope) || h()] ) diff --git a/src/utils/config-defaults.js b/src/utils/config-defaults.js index 410f382162c..e896f67248c 100644 --- a/src/utils/config-defaults.js +++ b/src/utils/config-defaults.js @@ -183,6 +183,9 @@ export default deepFreeze({ borderVariant: undefined, textVariant: undefined }, + BLink: { + routerComponentName: undefined + }, BListGroupItem: { variant: undefined }, diff --git a/src/utils/dom.js b/src/utils/dom.js index 59c6a0e0b42..b1857122606 100644 --- a/src/utils/dom.js +++ b/src/utils/dom.js @@ -2,6 +2,7 @@ import { from as arrayFrom } from './array' import { hasWindowSupport, hasDocumentSupport } from './env' import { isFunction, isNull } from './inspect' import { toFloat } from './number' +import { toString } from './string' // --- Constants --- @@ -74,6 +75,9 @@ export const getActiveElement = (excludes = []) => { return activeElement && !excludes.some(el => el === activeElement) ? activeElement : null } +// Returns `true` if a tag's name equals `name` +export const isTag = (tag, name) => toString(tag).toLowerCase() === toString(name).toLowerCase() + // Determine if an HTML element is the currently active element export const isActiveElement = el => isElement(el) && el === getActiveElement() diff --git a/src/utils/router.js b/src/utils/router.js index dc7be512648..521de9fd0bd 100644 --- a/src/utils/router.js +++ b/src/utils/router.js @@ -1,3 +1,4 @@ +import { isTag } from './dom' import { isArray, isNull, isPlainObject, isString, isUndefined } from './inspect' import { keys } from './object' import { toString } from './string' @@ -87,14 +88,25 @@ export const parseQuery = query => { return parsed } -export const isRouterLink = tag => toString(tag).toLowerCase() !== ANCHOR_TAG +export const isLink = props => !!(props.href || props.to) -export const computeTag = ({ to, disabled } = {}, thisOrParent) => { - return thisOrParent.$router && to && !disabled - ? thisOrParent.$nuxt - ? 'nuxt-link' - : 'router-link' - : ANCHOR_TAG +export const isRouterLink = tag => !isTag(tag, ANCHOR_TAG) + +export const computeTag = ({ to, disabled, routerComponentName } = {}, thisOrParent) => { + const hasRouter = thisOrParent.$router + if (!hasRouter || (hasRouter && disabled) || (hasRouter && !to)) { + return ANCHOR_TAG + } + + // TODO: + // Check registered components for existence of user supplied router link component name + // We would need to check PascalCase, kebab-case, and camelCase versions of name: + // const name = routerComponentName + // const names = [name, PascalCase(name), KebabCase(name), CamelCase(name)] + // exists = names.some(name => !!thisOrParent.$options.components[name]) + // And may want to cache the result for performance or we just let the render fail + // if the component is not registered + return routerComponentName || (thisOrParent.$nuxt ? 'nuxt-link' : 'router-link') } export const computeRel = ({ target, rel } = {}) => {