diff --git a/docs/common-props.json b/docs/common-props.json index c811f60b7a9..d728326c297 100644 --- a/docs/common-props.json +++ b/docs/common-props.json @@ -230,6 +230,9 @@ "event": { "description": "router-link 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'" + }, "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" } diff --git a/docs/markdown/reference/router-links/README.md b/docs/markdown/reference/router-links/README.md index d898c8edb1a..bc93b575ee1 100644 --- a/docs/markdown/reference/router-links/README.md +++ b/docs/markdown/reference/router-links/README.md @@ -155,19 +155,18 @@ render a [``](https://nuxtjs.org/api/components-nuxt-link) sub compon ``. `` supports all of the above router link props, plus the following additional Nuxt.js specific props. -### `no-prefetch` +### `prefetch` - type: `boolean` -- default: `false` -- availability: Nuxt.js 2.4.0+ +- default: `undefined` +- availability: Nuxt.js 2.10.0+ 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. +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` configured in the +`nuxt.config.js` configuration file. -**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. +**Note:** If you have are using a version of Nuxt.js `< 2.10.0`, then this prop will have no effect. Prefetching support requires [IntersectionObserver](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) @@ -187,3 +186,17 @@ export default { } } ``` + +### `no-prefetch` + +- type: `boolean` +- default: `false` +- availability: Nuxt.js 2.4.0+ + +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. + +**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. diff --git a/src/components/avatar/avatar.js b/src/components/avatar/avatar.js index 9661d0e4892..6501aede88e 100644 --- a/src/components/avatar/avatar.js +++ b/src/components/avatar/avatar.js @@ -1,308 +1,279 @@ -import Vue from '../../utils/vue' -import pluckProps from '../../utils/pluck-props' -import { getComponentConfig } from '../../utils/config' -import { isNumber, isString, isUndefinedOrNull } from '../../utils/inspect' -import { toFloat } from '../../utils/number' -import { BButton } from '../button/button' -import { BLink } from '../link/link' -import { BIcon } from '../../icons/icon' -import { BIconPersonFill } from '../../icons/icons' -import normalizeSlotMixin from '../../mixins/normalize-slot' - -// --- Constants --- -const NAME = 'BAvatar' -const CLASS_NAME = 'b-avatar' - -const RX_NUMBER = /^[0-9]*\.?[0-9]+$/ - -const FONT_SIZE_SCALE = 0.4 -const BADGE_FONT_SIZE_SCALE = FONT_SIZE_SCALE * 0.7 - -const DEFAULT_SIZES = { - sm: '1.5em', - md: '2.5em', - lg: '3.5em' -} - -// --- Props --- -const linkProps = { - href: { - type: String - // default: null - }, - to: { - type: [String, Object] - // default: null - }, - append: { - type: Boolean, - default: false - }, - replace: { - type: Boolean, - default: false - }, - disabled: { - type: Boolean, - default: false - }, - rel: { - type: String - // default: null - }, - target: { - type: String - // default: null - }, - activeClass: { - type: String - // default: null - }, - exact: { - type: Boolean, - default: false - }, - exactActiveClass: { - type: String - // default: null - }, - noPrefetch: { - type: Boolean, - default: false - } -} - -const props = { - src: { - type: String - // default: null - }, - text: { - type: String - // default: null - }, - icon: { - type: String - // default: null - }, - alt: { - type: String, - default: 'avatar' - }, - variant: { - type: String, - default: () => getComponentConfig(NAME, 'variant') - }, - size: { - type: [Number, String], - default: null - }, - square: { - type: Boolean, - default: false - }, - rounded: { - type: [Boolean, String], - default: false - }, - button: { - type: Boolean, - default: false - }, - buttonType: { - type: String, - default: 'button' - }, - badge: { - type: [Boolean, String], - default: false - }, - badgeVariant: { - type: String, - default: () => getComponentConfig(NAME, 'badgeVariant') - }, - badgeTop: { - type: Boolean, - default: false - }, - badgeLeft: { - type: Boolean, - default: false - }, - badgeOffset: { - type: String, - default: '0px' - }, - ...linkProps, - ariaLabel: { - type: String - // default: null - } -} - -// --- Utility methods --- -export const computeSize = value => { - // Default to `md` size when `null`, or parse to - // number when value is a float-like string - value = - isUndefinedOrNull(value) || value === '' - ? 'md' - : isString(value) && RX_NUMBER.test(value) - ? toFloat(value, 0) - : value - // Convert all numbers to pixel values - // Handle default sizes when `sm`, `md` or `lg` - // Or use value as is - return isNumber(value) ? `${value}px` : DEFAULT_SIZES[value] || value -} - -// --- Main component --- -// @vue/component -export const BAvatar = /*#__PURE__*/ Vue.extend({ - name: NAME, - mixins: [normalizeSlotMixin], - inject: { - bvAvatarGroup: { default: null } - }, - props, - data() { - return { - localSrc: this.src || null - } - }, - computed: { - computedSize() { - // Always use the avatar group size - return computeSize(this.bvAvatarGroup ? this.bvAvatarGroup.size : this.size) - }, - computedVariant() { - // Prefer avatar-group variant if provided - const avatarGroup = this.bvAvatarGroup - return avatarGroup && avatarGroup.variant ? avatarGroup.variant : this.variant - }, - computedRounded() { - const avatarGroup = this.bvAvatarGroup - const square = avatarGroup && avatarGroup.square ? true : this.square - const rounded = avatarGroup && avatarGroup.rounded ? avatarGroup.rounded : this.rounded - return square ? '0' : rounded === '' ? true : rounded || 'circle' - }, - fontStyle() { - let fontSize = this.computedSize - fontSize = fontSize ? `calc(${fontSize} * ${FONT_SIZE_SCALE})` : null - return fontSize ? { fontSize } : {} - }, - marginStyle() { - const avatarGroup = this.bvAvatarGroup - const overlapScale = avatarGroup ? avatarGroup.overlapScale : 0 - const size = this.computedSize - const value = size && overlapScale ? `calc(${size} * -${overlapScale})` : null - return value ? { marginLeft: value, marginRight: value } : {} - }, - badgeStyle() { - const { computedSize: size, badgeTop, badgeLeft, badgeOffset } = this - const offset = badgeOffset || '0px' - return { - fontSize: size ? `calc(${size} * ${BADGE_FONT_SIZE_SCALE} )` : null, - top: badgeTop ? offset : null, - bottom: badgeTop ? null : offset, - left: badgeLeft ? offset : null, - right: badgeLeft ? null : offset - } - } - }, - watch: { - src(newSrc, oldSrc) { - if (newSrc !== oldSrc) { - this.localSrc = newSrc || null - } - } - }, - methods: { - onImgError(evt) { - this.localSrc = null - this.$emit('img-error', evt) - }, - onClick(evt) { - this.$emit('click', evt) - } - }, - render(h) { - const { - computedVariant: variant, - disabled, - computedRounded: rounded, - icon, - localSrc: src, - text, - fontStyle, - marginStyle, - computedSize: size, - button: isButton, - buttonType: type, - badge, - badgeVariant, - badgeStyle - } = this - const isBLink = !isButton && (this.href || this.to) - const tag = isButton ? BButton : isBLink ? BLink : 'span' - const alt = this.alt || null - const ariaLabel = this.ariaLabel || null - - let $content = null - if (this.hasNormalizedSlot('default')) { - // Default slot overrides props - $content = h('span', { staticClass: 'b-avatar-custom' }, [this.normalizeSlot('default')]) - } else if (src) { - $content = h('img', { - style: variant ? {} : { width: '100%', height: '100%' }, - attrs: { src, alt }, - on: { error: this.onImgError } - }) - $content = h('span', { staticClass: 'b-avatar-img' }, [$content]) - } else if (icon) { - $content = h(BIcon, { - props: { icon }, - attrs: { 'aria-hidden': 'true', alt } - }) - } else if (text) { - $content = h('span', { staticClass: 'b-avatar-text', style: fontStyle }, [h('span', text)]) - } else { - // Fallback default avatar content - $content = h(BIconPersonFill, { attrs: { 'aria-hidden': 'true', alt } }) - } - - let $badge = h() - const hasBadgeSlot = this.hasNormalizedSlot('badge') - if (badge || badge === '' || hasBadgeSlot) { - const badgeText = badge === true ? '' : badge - $badge = h( - 'span', - { - staticClass: 'b-avatar-badge', - class: { [`badge-${badgeVariant}`]: !!badgeVariant }, - style: badgeStyle - }, - [hasBadgeSlot ? this.normalizeSlot('badge') : badgeText] - ) - } - - const componentData = { - staticClass: CLASS_NAME, - class: { - // We use badge styles for theme variants when not rendering `BButton` - [`badge-${variant}`]: !isButton && variant, - // Rounding/Square - rounded: rounded === true, - [`rounded-${rounded}`]: rounded && rounded !== true, - // Other classes - disabled - }, - 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 } : {} - } - - return h(tag, componentData, [$content, $badge]) - } -}) +import Vue from '../../utils/vue' +import pluckProps from '../../utils/pluck-props' +import { getComponentConfig } from '../../utils/config' +import { isNumber, isString, isUndefinedOrNull } from '../../utils/inspect' +import { toFloat } from '../../utils/number' +import { BButton } from '../button/button' +import { BLink, props as BLinkProps } from '../link/link' +import { BIcon } from '../../icons/icon' +import { BIconPersonFill } from '../../icons/icons' +import normalizeSlotMixin from '../../mixins/normalize-slot' + +// --- Constants --- +const NAME = 'BAvatar' +const CLASS_NAME = 'b-avatar' + +const RX_NUMBER = /^[0-9]*\.?[0-9]+$/ + +const FONT_SIZE_SCALE = 0.4 +const BADGE_FONT_SIZE_SCALE = FONT_SIZE_SCALE * 0.7 + +const DEFAULT_SIZES = { + sm: '1.5em', + md: '2.5em', + lg: '3.5em' +} + +// --- Props --- +const linkProps = pluckProps( + [ + 'href', + 'rel', + 'target', + 'disabled', + 'to', + 'append', + 'replace', + 'activeClass', + 'exact', + 'exactActiveClass', + 'prefetch', + 'noPrefetch' + ], + BLinkProps +) + +const props = { + src: { + type: String + // default: null + }, + text: { + type: String + // default: null + }, + icon: { + type: String + // default: null + }, + alt: { + type: String, + default: 'avatar' + }, + variant: { + type: String, + default: () => getComponentConfig(NAME, 'variant') + }, + size: { + type: [Number, String], + default: null + }, + square: { + type: Boolean, + default: false + }, + rounded: { + type: [Boolean, String], + default: false + }, + button: { + type: Boolean, + default: false + }, + buttonType: { + type: String, + default: 'button' + }, + badge: { + type: [Boolean, String], + default: false + }, + badgeVariant: { + type: String, + default: () => getComponentConfig(NAME, 'badgeVariant') + }, + badgeTop: { + type: Boolean, + default: false + }, + badgeLeft: { + type: Boolean, + default: false + }, + badgeOffset: { + type: String, + default: '0px' + }, + ...linkProps, + ariaLabel: { + type: String + // default: null + } +} + +// --- Utility methods --- +export const computeSize = value => { + // Default to `md` size when `null`, or parse to + // number when value is a float-like string + value = + isUndefinedOrNull(value) || value === '' + ? 'md' + : isString(value) && RX_NUMBER.test(value) + ? toFloat(value, 0) + : value + // Convert all numbers to pixel values + // Handle default sizes when `sm`, `md` or `lg` + // Or use value as is + return isNumber(value) ? `${value}px` : DEFAULT_SIZES[value] || value +} + +// --- Main component --- +// @vue/component +export const BAvatar = /*#__PURE__*/ Vue.extend({ + name: NAME, + mixins: [normalizeSlotMixin], + inject: { + bvAvatarGroup: { default: null } + }, + props, + data() { + return { + localSrc: this.src || null + } + }, + computed: { + computedSize() { + // Always use the avatar group size + return computeSize(this.bvAvatarGroup ? this.bvAvatarGroup.size : this.size) + }, + computedVariant() { + // Prefer avatar-group variant if provided + const avatarGroup = this.bvAvatarGroup + return avatarGroup && avatarGroup.variant ? avatarGroup.variant : this.variant + }, + computedRounded() { + const avatarGroup = this.bvAvatarGroup + const square = avatarGroup && avatarGroup.square ? true : this.square + const rounded = avatarGroup && avatarGroup.rounded ? avatarGroup.rounded : this.rounded + return square ? '0' : rounded === '' ? true : rounded || 'circle' + }, + fontStyle() { + let fontSize = this.computedSize + fontSize = fontSize ? `calc(${fontSize} * ${FONT_SIZE_SCALE})` : null + return fontSize ? { fontSize } : {} + }, + marginStyle() { + const avatarGroup = this.bvAvatarGroup + const overlapScale = avatarGroup ? avatarGroup.overlapScale : 0 + const size = this.computedSize + const value = size && overlapScale ? `calc(${size} * -${overlapScale})` : null + return value ? { marginLeft: value, marginRight: value } : {} + }, + badgeStyle() { + const { computedSize: size, badgeTop, badgeLeft, badgeOffset } = this + const offset = badgeOffset || '0px' + return { + fontSize: size ? `calc(${size} * ${BADGE_FONT_SIZE_SCALE} )` : null, + top: badgeTop ? offset : null, + bottom: badgeTop ? null : offset, + left: badgeLeft ? offset : null, + right: badgeLeft ? null : offset + } + } + }, + watch: { + src(newSrc, oldSrc) { + if (newSrc !== oldSrc) { + this.localSrc = newSrc || null + } + } + }, + methods: { + onImgError(evt) { + this.localSrc = null + this.$emit('img-error', evt) + }, + onClick(evt) { + this.$emit('click', evt) + } + }, + render(h) { + const { + computedVariant: variant, + disabled, + computedRounded: rounded, + icon, + localSrc: src, + text, + fontStyle, + marginStyle, + computedSize: size, + button: isButton, + buttonType: type, + badge, + badgeVariant, + badgeStyle + } = this + const isBLink = !isButton && (this.href || this.to) + const tag = isButton ? BButton : isBLink ? BLink : 'span' + const alt = this.alt || null + const ariaLabel = this.ariaLabel || null + + let $content = null + if (this.hasNormalizedSlot('default')) { + // Default slot overrides props + $content = h('span', { staticClass: 'b-avatar-custom' }, [this.normalizeSlot('default')]) + } else if (src) { + $content = h('img', { + style: variant ? {} : { width: '100%', height: '100%' }, + attrs: { src, alt }, + on: { error: this.onImgError } + }) + $content = h('span', { staticClass: 'b-avatar-img' }, [$content]) + } else if (icon) { + $content = h(BIcon, { + props: { icon }, + attrs: { 'aria-hidden': 'true', alt } + }) + } else if (text) { + $content = h('span', { staticClass: 'b-avatar-text', style: fontStyle }, [h('span', text)]) + } else { + // Fallback default avatar content + $content = h(BIconPersonFill, { attrs: { 'aria-hidden': 'true', alt } }) + } + + let $badge = h() + const hasBadgeSlot = this.hasNormalizedSlot('badge') + if (badge || badge === '' || hasBadgeSlot) { + const badgeText = badge === true ? '' : badge + $badge = h( + 'span', + { + staticClass: 'b-avatar-badge', + class: { [`badge-${badgeVariant}`]: !!badgeVariant }, + style: badgeStyle + }, + [hasBadgeSlot ? this.normalizeSlot('badge') : badgeText] + ) + } + + const componentData = { + staticClass: CLASS_NAME, + class: { + // We use badge styles for theme variants when not rendering `BButton` + [`badge-${variant}`]: !isButton && variant, + // Rounding/Square + rounded: rounded === true, + [`rounded-${rounded}`]: rounded && rounded !== true, + // Other classes + disabled + }, + 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 } : {} + } + + return h(tag, componentData, [$content, $badge]) + } +}) diff --git a/src/components/link/link.js b/src/components/link/link.js index cd5b2034ab6..05b622ee40e 100644 --- a/src/components/link/link.js +++ b/src/components/link/link.js @@ -39,7 +39,7 @@ export const propsFactory = () => ({ type: Boolean, default: false }, - // router-link specific props + // specific props to: { type: [String, Object], default: null @@ -72,7 +72,13 @@ export const propsFactory = () => ({ type: String, default: 'a' }, - // nuxt-link specific prop(s) + // specific prop(s) + prefetch: { + type: Boolean + // Must be `undefined` to fall back to the value defined in the + // `nuxt.config.js` configuration file for `router.prefetchLinks` + // default: undefined + }, noPrefetch: { type: Boolean, default: false diff --git a/src/components/pagination-nav/README.md b/src/components/pagination-nav/README.md index 9dd6b79c5f0..c4a8e9be71a 100644 --- a/src/components/pagination-nav/README.md +++ b/src/components/pagination-nav/README.md @@ -59,6 +59,7 @@ The following router link specific props are supported: - `active-class` - `exact` - `exact-active-class` +- `prefetch` (`` specific prop) - `no-prefetch` (`` specific prop) For details on the above props, refer to the [Router Link Support](/docs/reference/router-links) diff --git a/src/components/pagination-nav/pagination-nav.js b/src/components/pagination-nav/pagination-nav.js index 3dbfa4378d3..b3b2678f8b9 100644 --- a/src/components/pagination-nav/pagination-nav.js +++ b/src/components/pagination-nav/pagination-nav.js @@ -1,5 +1,6 @@ import Vue from '../../utils/vue' import looseEqual from '../../utils/loose-equal' +import pluckProps from '../../utils/pluck-props' import { getComponentConfig } from '../../utils/config' import { attemptBlur, requestAF } from '../../utils/dom' import { isBrowser } from '../../utils/env' @@ -10,6 +11,7 @@ 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' const NAME = 'BPaginationNav' @@ -59,24 +61,7 @@ const props = { type: Boolean, default: false }, - // router-link specific props - activeClass: { - type: String - // default: undefined - }, - exact: { - type: Boolean, - default: false - }, - exactActiveClass: { - type: String - // default: undefined - }, - // nuxt-link specific prop(s) - noPrefetch: { - type: Boolean, - default: false - } + ...pluckProps(['activeClass', 'exact', 'exactActiveClass', 'prefetch', 'noPrefetch'], BLinkProps) } // The render function is brought in via the pagination mixin @@ -191,24 +176,37 @@ export const BPaginationNav = /*#__PURE__*/ Vue.extend({ }, linkProps(pageNum) { 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: this.disabled, - // The following props are only used if BLink detects router - exact: this.exact, - activeClass: this.activeClass, - exactActiveClass: this.exactActiveClass, - append: this.append, - replace: this.replace, - // nuxt-link specific prop - noPrefetch: this.noPrefetch + 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 = '') {