bootstrap-vue
Version:
With more than 85 components, over 45 available plugins, several directives, and 1000+ icons, BootstrapVue provides one of the most comprehensive implementations of the Bootstrap v4 component and grid system available for Vue.js v2.6, complete with extens
254 lines (242 loc) • 7.04 kB
JavaScript
import Vue from '../../vue'
import { NAME_AVATAR } from '../../constants/components'
import { RX_NUMBER } from '../../constants/regex'
import { getComponentConfig } from '../../utils/config'
import { isNumber, isString } from '../../utils/inspect'
import { toFloat } from '../../utils/number'
import { omit } from '../../utils/object'
import { pluckProps } from '../../utils/props'
import { isLink } from '../../utils/router'
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 CLASS_NAME = 'b-avatar'
const SIZES = ['sm', null, 'lg']
const FONT_SIZE_SCALE = 0.4
const BADGE_FONT_SIZE_SCALE = FONT_SIZE_SCALE * 0.7
// --- Props ---
const linkProps = omit(BLinkProps, ['active', 'event', 'routerTag'])
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_AVATAR, '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_AVATAR, '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 => {
// Parse to number when value is a float-like string
value = isString(value) && RX_NUMBER.test(value) ? toFloat(value, 0) : value
// Convert all numbers to pixel values
return isNumber(value) ? `${value}px` : value || null
}
// --- Main component ---
// @vue/component
export const BAvatar = /*#__PURE__*/ Vue.extend({
name: NAME_AVATAR,
mixins: [normalizeSlotMixin],
inject: {
bvAvatarGroup: { default: null }
},
props,
data() {
return {
localSrc: this.src || null
}
},
computed: {
computedSize() {
// Always use the avatar group size
const { bvAvatarGroup } = this
return computeSize(bvAvatarGroup ? bvAvatarGroup.size : this.size)
},
computedVariant() {
const { bvAvatarGroup } = this
return bvAvatarGroup && bvAvatarGroup.variant ? bvAvatarGroup.variant : this.variant
},
computedRounded() {
const { bvAvatarGroup } = this
const square = bvAvatarGroup && bvAvatarGroup.square ? true : this.square
const rounded = bvAvatarGroup && bvAvatarGroup.rounded ? bvAvatarGroup.rounded : this.rounded
return square ? '0' : rounded === '' ? true : rounded || 'circle'
},
fontStyle() {
const { computedSize: size } = this
const fontSize = SIZES.indexOf(size) === -1 ? `calc(${size} * ${FONT_SIZE_SCALE})` : null
return fontSize ? { fontSize } : {}
},
marginStyle() {
const { computedSize: size, bvAvatarGroup } = this
const overlapScale = bvAvatarGroup ? bvAvatarGroup.overlapScale : 0
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: SIZES.indexOf(size) === -1 ? `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,
buttonType: type,
badge,
badgeVariant,
badgeStyle
} = this
const link = !button && isLink(this)
const tag = button ? BButton : link ? BLink : 'span'
const alt = this.alt
const ariaLabel = this.ariaLabel || null
let $content = null
if (this.hasNormalizedSlot()) {
// Default slot overrides props
$content = h('span', { staticClass: 'b-avatar-custom' }, [this.normalizeSlot()])
} 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: {
// Apply size class
[`${CLASS_NAME}-${size}`]: size && SIZES.indexOf(size) !== -1,
// We use badge styles for theme variants when not rendering `BButton`
[`badge-${variant}`]: !button && variant,
// Rounding/Square
rounded: rounded === true,
[`rounded-${rounded}`]: rounded && rounded !== true,
// Other classes
disabled
},
style: { ...marginStyle, width: size, height: size },
attrs: { 'aria-label': ariaLabel || null },
props: button ? { variant, disabled, type } : link ? pluckProps(linkProps, this) : {},
on: button || link ? { click: this.onClick } : {}
}
return h(tag, componentData, [$content, $badge])
}
})