UNPKG

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

457 lines (445 loc) 12.2 kB
import Vue from '../../vue' import { NAME_FORM_RATING, NAME_FORM_RATING_STAR } from '../../constants/components' import { CODE_LEFT, CODE_RIGHT, CODE_UP, CODE_DOWN } from '../../constants/key-codes' import { arrayIncludes, concat } from '../../utils/array' import { getComponentConfig } from '../../utils/config' import { attemptBlur, attemptFocus } from '../../utils/dom' import { stopEvent } from '../../utils/events' import { isNull } from '../../utils/inspect' import { isLocaleRTL } from '../../utils/locale' import { mathMax, mathMin } from '../../utils/math' import { toInteger, toFloat } from '../../utils/number' import { toString } from '../../utils/string' import identity from '../../utils/identity' import idMixin from '../../mixins/id' import normalizeSlotMixin from '../../mixins/normalize-slot' import { BIcon } from '../../icons/icon' import { BIconStar, BIconStarHalf, BIconStarFill, BIconX } from '../../icons/icons' // --- Constants --- const MIN_STARS = 3 const DEFAULT_STARS = 5 // --- Private helper component --- // @vue/component const BVFormRatingStar = Vue.extend({ name: NAME_FORM_RATING_STAR, mixins: [normalizeSlotMixin], props: { rating: { type: Number, default: 0 }, star: { type: Number, default: 0 }, focused: { // If parent is focused type: Boolean, default: false }, variant: { type: String // default: null }, disabled: { type: Boolean, default: false }, readonly: { type: Boolean, default: false }, hasClear: { type: Boolean, default: false } }, methods: { onClick(evt) { if (!this.disabled && !this.readonly) { stopEvent(evt, { propagation: false }) this.$emit('selected', this.star) } } }, render(h) { const { rating, star, focused, hasClear, variant, disabled, readonly } = this const minStar = hasClear ? 0 : 1 const type = rating >= star ? 'full' : rating >= star - 0.5 ? 'half' : 'empty' const slotScope = { variant, disabled, readonly } return h( 'span', { staticClass: 'b-rating-star', class: { // When not hovered, we use this class to focus the current (or first) star focused: (focused && rating === star) || (!toInteger(rating) && star === minStar), // We add type classes to we can handle RTL styling 'b-rating-star-empty': type === 'empty', 'b-rating-star-half': type === 'half', 'b-rating-star-full': type === 'full' }, attrs: { tabindex: !disabled && !readonly ? '-1' : null }, on: { click: this.onClick } }, [h('span', { staticClass: 'b-rating-icon' }, [this.normalizeSlot(type, slotScope)])] ) } }) // --- Utility methods --- const computeStars = stars => mathMax(MIN_STARS, toInteger(stars, DEFAULT_STARS)) const clampValue = (value, min, max) => mathMax(mathMin(value, max), min) // --- BFormRating --- // @vue/component export const BFormRating = /*#__PURE__*/ Vue.extend({ name: NAME_FORM_RATING, components: { BIconStar, BIconStarHalf, BIconStarFill, BIconX }, mixins: [idMixin], model: { prop: 'value', event: 'change' }, props: { value: { type: [Number, String], default: null }, stars: { type: [Number, String], default: DEFAULT_STARS, validator: val => toInteger(val) >= MIN_STARS }, variant: { type: String, default: () => getComponentConfig(NAME_FORM_RATING, 'variant') }, color: { // CSS color string (overrides variant) type: String, default: () => getComponentConfig(NAME_FORM_RATING, 'color') }, showValue: { type: Boolean, default: false }, showValueMax: { type: Boolean, default: false }, disabled: { type: Boolean, default: false }, readonly: { type: Boolean, default: false }, size: { type: String // default: null }, name: { type: String // default: null }, form: { type: String // default: null }, noBorder: { type: Boolean, default: false }, inline: { type: Boolean, default: false }, precision: { type: [Number, String], default: null }, iconEmpty: { type: String, default: 'star' }, iconHalf: { type: String, default: 'star-half' }, iconFull: { type: String, default: 'star-fill' }, iconClear: { type: String, default: 'x' }, locale: { // Locale for the formatted value (if shown) // Defaults to the browser locale. Falls back to `en` type: [String, Array] // default: undefined }, showClear: { type: Boolean, default: false } }, data() { const value = toFloat(this.value, null) const stars = computeStars(this.stars) return { localValue: isNull(value) ? null : clampValue(value, 0, stars), hasFocus: false } }, computed: { computedStars() { return computeStars(this.stars) }, computedRating() { const value = toFloat(this.localValue, 0) const precision = toInteger(this.precision, 3) // We clamp the value between `0` and stars return clampValue(toFloat(value.toFixed(precision)), 0, this.computedStars) }, computedLocale() { const locales = concat(this.locale).filter(identity) const nf = new Intl.NumberFormat(locales) return nf.resolvedOptions().locale }, isInteractive() { return !this.disabled && !this.readonly }, isRTL() { return isLocaleRTL(this.computedLocale) }, formattedRating() { const precision = toInteger(this.precision) const showValueMax = this.showValueMax const locale = this.computedLocale const formatOptions = { notation: 'standard', minimumFractionDigits: isNaN(precision) ? 0 : precision, maximumFractionDigits: isNaN(precision) ? 3 : precision } const stars = this.computedStars.toLocaleString(locale) let value = this.localValue value = isNull(value) ? showValueMax ? '-' : '' : value.toLocaleString(locale, formatOptions) return showValueMax ? `${value}/${stars}` : value } }, watch: { value(newVal, oldVal) { if (newVal !== oldVal) { const value = toFloat(newVal, null) this.localValue = isNull(value) ? null : clampValue(value, 0, this.computedStars) } }, localValue(newVal, oldVal) { if (newVal !== oldVal && newVal !== (this.value || 0)) { this.$emit('change', newVal || null) } }, disabled(newVal) { if (newVal) { this.hasFocus = false this.blur() } } }, methods: { // --- Public methods --- focus() { if (!this.disabled) { attemptFocus(this.$el) } }, blur() { if (!this.disabled) { attemptBlur(this.$el) } }, // --- Private methods --- onKeydown(evt) { const { keyCode } = evt if ( this.isInteractive && arrayIncludes([CODE_LEFT, CODE_DOWN, CODE_RIGHT, CODE_UP], keyCode) ) { stopEvent(evt, { propagation: false }) const value = toInteger(this.localValue, 0) const min = this.showClear ? 0 : 1 const stars = this.computedStars // In RTL mode, LEFT/RIGHT are swapped const amountRtl = this.isRTL ? -1 : 1 if (keyCode === CODE_LEFT) { this.localValue = clampValue(value - amountRtl, min, stars) || null } else if (keyCode === CODE_RIGHT) { this.localValue = clampValue(value + amountRtl, min, stars) } else if (keyCode === CODE_DOWN) { this.localValue = clampValue(value - 1, min, stars) || null } else if (keyCode === CODE_UP) { this.localValue = clampValue(value + 1, min, stars) } } }, onSelected(value) { if (this.isInteractive) { this.localValue = value } }, onFocus(evt) { this.hasFocus = !this.isInteractive ? false : evt.type === 'focus' }, // --- Render methods --- renderIcon(icon) { return this.$createElement(BIcon, { props: { icon, variant: this.disabled || this.color ? null : this.variant || null } }) }, iconEmptyFn() { return this.renderIcon(this.iconEmpty) }, iconHalfFn() { return this.renderIcon(this.iconHalf) }, iconFullFn() { return this.renderIcon(this.iconFull) }, iconClearFn() { return this.$createElement(BIcon, { props: { icon: this.iconClear } }) } }, render(h) { const { disabled, readonly, size, name, form, inline, variant, color, noBorder, hasFocus, computedRating, computedStars, formattedRating, showClear, isRTL, isInteractive, $scopedSlots } = this const $content = [] if (showClear && !disabled && !readonly) { const $icon = h('span', { staticClass: 'b-rating-icon' }, [ ($scopedSlots['icon-clear'] || this.iconClearFn)() ]) $content.push( h( 'span', { staticClass: 'b-rating-star b-rating-star-clear flex-grow-1', class: { focused: hasFocus && computedRating === 0 }, attrs: { tabindex: isInteractive ? '-1' : null }, on: { click: () => this.onSelected(null) }, key: 'clear' }, [$icon] ) ) } for (let index = 0; index < computedStars; index++) { const value = index + 1 $content.push( h(BVFormRatingStar, { staticClass: 'flex-grow-1', style: color && !disabled ? { color } : {}, props: { rating: computedRating, star: value, variant: disabled ? null : variant || null, disabled, readonly, focused: hasFocus, hasClear: showClear }, on: { selected: this.onSelected }, scopedSlots: { empty: $scopedSlots['icon-empty'] || this.iconEmptyFn, half: $scopedSlots['icon-half'] || this.iconHalfFn, full: $scopedSlots['icon-full'] || this.iconFullFn }, key: index }) ) } if (name) { $content.push( h('input', { attrs: { type: 'hidden', value: isNull(this.localValue) ? '' : computedRating, name, form: form || null }, key: 'hidden' }) ) } if (this.showValue) { $content.push( h( 'b', { staticClass: 'b-rating-value flex-grow-1', attrs: { 'aria-hidden': 'true' }, key: 'value' }, toString(formattedRating) ) ) } return h( 'output', { staticClass: 'b-rating form-control align-items-center', class: { [`form-control-${size}`]: !!size, 'd-inline-flex': inline, 'd-flex': !inline, 'border-0': noBorder, disabled, readonly: !disabled && readonly }, attrs: { id: this.safeId(), dir: isRTL ? 'rtl' : 'ltr', tabindex: disabled ? null : '0', disabled, role: 'slider', 'aria-disabled': disabled ? 'true' : null, 'aria-readonly': !disabled && readonly ? 'true' : null, 'aria-live': 'off', 'aria-valuemin': showClear ? '0' : '1', 'aria-valuemax': toString(computedStars), 'aria-valuenow': computedRating ? toString(computedRating) : null }, on: { keydown: this.onKeydown, focus: this.onFocus, blur: this.onFocus } }, $content ) } })