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

355 lines (310 loc) 15.6 kB
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); enumerableOnly && (symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; })), keys.push.apply(keys, symbols); } return keys; } function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = null != arguments[i] ? arguments[i] : {}; i % 2 ? ownKeys(Object(source), !0).forEach(function (key) { _defineProperty(target, key, source[key]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } return target; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } import { NAME_FORM_GROUP } from '../../constants/components'; import { IS_BROWSER } from '../../constants/env'; import { PROP_TYPE_ARRAY_OBJECT_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_BOOLEAN_NUMBER_STRING, PROP_TYPE_STRING } from '../../constants/props'; import { RX_SPACE_SPLIT } from '../../constants/regex'; import { SLOT_NAME_DEFAULT, SLOT_NAME_DESCRIPTION, SLOT_NAME_INVALID_FEEDBACK, SLOT_NAME_LABEL, SLOT_NAME_VALID_FEEDBACK } from '../../constants/slots'; import { arrayIncludes } from '../../utils/array'; import { getBreakpointsUpCached } from '../../utils/config'; import { cssEscape } from '../../utils/css-escape'; import { select, selectAll, isVisible, setAttr, removeAttr, getAttr, attemptFocus } from '../../utils/dom'; import { identity } from '../../utils/identity'; import { isBoolean } from '../../utils/inspect'; import { toInteger } from '../../utils/number'; import { create, keys, sortKeys } from '../../utils/object'; import { makeProp, makePropsConfigurable, suffixPropName } from '../../utils/props'; import { formStateMixin, props as formStateProps } from '../../mixins/form-state'; import { idMixin, props as idProps } from '../../mixins/id'; import { normalizeSlotMixin } from '../../mixins/normalize-slot'; import { BCol } from '../layout/col'; import { BFormRow } from '../layout/form-row'; import { BFormText } from '../form/form-text'; import { BFormInvalidFeedback } from '../form/form-invalid-feedback'; import { BFormValidFeedback } from '../form/form-valid-feedback'; // --- Constants --- var INPUTS = ['input', 'select', 'textarea']; // Selector for finding first input in the form group var INPUT_SELECTOR = INPUTS.map(function (v) { return "".concat(v, ":not([disabled])"); }).join(); // A list of interactive elements (tag names) inside `<b-form-group>`'s legend var LEGEND_INTERACTIVE_ELEMENTS = [].concat(INPUTS, ['a', 'button', 'label']); // --- Props --- // Prop generator for lazy generation of props export var generateProps = function generateProps() { return makePropsConfigurable(sortKeys(_objectSpread(_objectSpread(_objectSpread(_objectSpread({}, idProps), formStateProps), getBreakpointsUpCached().reduce(function (props, breakpoint) { // i.e. 'content-cols', 'content-cols-sm', 'content-cols-md', ... props[suffixPropName(breakpoint, 'contentCols')] = makeProp(PROP_TYPE_BOOLEAN_NUMBER_STRING); // i.e. 'label-align', 'label-align-sm', 'label-align-md', ... props[suffixPropName(breakpoint, 'labelAlign')] = makeProp(PROP_TYPE_STRING); // i.e. 'label-cols', 'label-cols-sm', 'label-cols-md', ... props[suffixPropName(breakpoint, 'labelCols')] = makeProp(PROP_TYPE_BOOLEAN_NUMBER_STRING); return props; }, create(null))), {}, { description: makeProp(PROP_TYPE_STRING), disabled: makeProp(PROP_TYPE_BOOLEAN, false), feedbackAriaLive: makeProp(PROP_TYPE_STRING, 'assertive'), invalidFeedback: makeProp(PROP_TYPE_STRING), label: makeProp(PROP_TYPE_STRING), labelClass: makeProp(PROP_TYPE_ARRAY_OBJECT_STRING), labelFor: makeProp(PROP_TYPE_STRING), labelSize: makeProp(PROP_TYPE_STRING), labelSrOnly: makeProp(PROP_TYPE_BOOLEAN, false), tooltip: makeProp(PROP_TYPE_BOOLEAN, false), validFeedback: makeProp(PROP_TYPE_STRING), validated: makeProp(PROP_TYPE_BOOLEAN, false) })), NAME_FORM_GROUP); }; // --- Main component --- // We do not use `extend()` here as that would evaluate the props // immediately, which we do not want to happen // @vue/component export var BFormGroup = { name: NAME_FORM_GROUP, mixins: [idMixin, formStateMixin, normalizeSlotMixin], get props() { // Allow props to be lazy evaled on first access and // then they become a non-getter afterwards // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get#Smart_self-overwriting_lazy_getters delete this.props; // eslint-disable-next-line no-return-assign return this.props = generateProps(); }, data: function data() { return { ariaDescribedby: null }; }, computed: { contentColProps: function contentColProps() { return this.getColProps(this.$props, 'content'); }, labelAlignClasses: function labelAlignClasses() { return this.getAlignClasses(this.$props, 'label'); }, labelColProps: function labelColProps() { return this.getColProps(this.$props, 'label'); }, isHorizontal: function isHorizontal() { // Determine if the form group will be rendered horizontal // based on the existence of 'content-col' or 'label-col' props return keys(this.contentColProps).length > 0 || keys(this.labelColProps).length > 0; } }, watch: { ariaDescribedby: function ariaDescribedby(newValue, oldValue) { if (newValue !== oldValue) { this.updateAriaDescribedby(newValue, oldValue); } } }, mounted: function mounted() { var _this = this; this.$nextTick(function () { // Set `aria-describedby` on the input specified by `labelFor` // We do this in a `$nextTick()` to ensure the children have finished rendering _this.updateAriaDescribedby(_this.ariaDescribedby); }); }, methods: { getAlignClasses: function getAlignClasses(props, prefix) { return getBreakpointsUpCached().reduce(function (result, breakpoint) { var propValue = props[suffixPropName(breakpoint, "".concat(prefix, "Align"))] || null; if (propValue) { result.push(['text', breakpoint, propValue].filter(identity).join('-')); } return result; }, []); }, getColProps: function getColProps(props, prefix) { return getBreakpointsUpCached().reduce(function (result, breakpoint) { var propValue = props[suffixPropName(breakpoint, "".concat(prefix, "Cols"))]; // Handle case where the prop's value is an empty string, // which represents `true` propValue = propValue === '' ? true : propValue || false; if (!isBoolean(propValue) && propValue !== 'auto') { // Convert to column size to number propValue = toInteger(propValue, 0); // Ensure column size is greater than `0` propValue = propValue > 0 ? propValue : false; } // Add the prop to the list of props to give to `<b-col>` // If breakpoint is '' (`${prefix}Cols` is `true`), then we use // the 'col' prop to make equal width at 'xs' if (propValue) { result[breakpoint || (isBoolean(propValue) ? 'col' : 'cols')] = propValue; } return result; }, {}); }, // Sets the `aria-describedby` attribute on the input if `labelFor` is set // Optionally accepts a string of IDs to remove as the second parameter // Preserves any `aria-describedby` value(s) user may have on input updateAriaDescribedby: function updateAriaDescribedby(newValue, oldValue) { var labelFor = this.labelFor; if (IS_BROWSER && labelFor) { // We need to escape `labelFor` since it can be user-provided var $input = select("#".concat(cssEscape(labelFor)), this.$refs.content); if ($input) { var attr = 'aria-describedby'; var newIds = (newValue || '').split(RX_SPACE_SPLIT); var oldIds = (oldValue || '').split(RX_SPACE_SPLIT); // Update ID list, preserving any original IDs // and ensuring the ID's are unique var ids = (getAttr($input, attr) || '').split(RX_SPACE_SPLIT).filter(function (id) { return !arrayIncludes(oldIds, id); }).concat(newIds).filter(function (id, index, ids) { return ids.indexOf(id) === index; }).filter(identity).join(' ').trim(); if (ids) { setAttr($input, attr, ids); } else { removeAttr($input, attr); } } } }, onLegendClick: function onLegendClick(event) { // Don't do anything if `labelFor` is set /* istanbul ignore next: clicking a label will focus the input, so no need to test */ if (this.labelFor) { return; } var target = event.target; var tagName = target ? target.tagName : ''; // If clicked an interactive element inside legend, // we just let the default happen /* istanbul ignore next */ if (LEGEND_INTERACTIVE_ELEMENTS.indexOf(tagName) !== -1) { return; } // If only a single input, focus it, emulating label behaviour var inputs = selectAll(INPUT_SELECTOR, this.$refs.content).filter(isVisible); if (inputs.length === 1) { attemptFocus(inputs[0]); } } }, render: function render(h) { var state = this.computedState, feedbackAriaLive = this.feedbackAriaLive, isHorizontal = this.isHorizontal, labelFor = this.labelFor, normalizeSlot = this.normalizeSlot, safeId = this.safeId, tooltip = this.tooltip; var id = safeId(); var isFieldset = !labelFor; var $label = h(); var labelContent = normalizeSlot(SLOT_NAME_LABEL) || this.label; var labelId = labelContent ? safeId('_BV_label_') : null; if (labelContent || isHorizontal) { var labelSize = this.labelSize, labelColProps = this.labelColProps; var labelTag = isFieldset ? 'legend' : 'label'; if (this.labelSrOnly) { if (labelContent) { $label = h(labelTag, { class: 'sr-only', attrs: { id: labelId, for: labelFor || null } }, [labelContent]); } $label = h(isHorizontal ? BCol : 'div', { props: isHorizontal ? labelColProps : {} }, [$label]); } else { $label = h(isHorizontal ? BCol : labelTag, { on: isFieldset ? { click: this.onLegendClick } : {}, props: isHorizontal ? _objectSpread(_objectSpread({}, labelColProps), {}, { tag: labelTag }) : {}, attrs: { id: labelId, for: labelFor || null, // We add a `tabindex` to legend so that screen readers // will properly read the `aria-labelledby` in IE tabindex: isFieldset ? '-1' : null }, class: [// Hide the focus ring on the legend isFieldset ? 'bv-no-focus-ring' : '', // When horizontal or if a legend is rendered, add 'col-form-label' class // for correct sizing as Bootstrap has inconsistent font styling for // legend in non-horizontal form groups // See: https://github.com/twbs/bootstrap/issues/27805 isHorizontal || isFieldset ? 'col-form-label' : '', // Emulate label padding top of `0` on legend when not horizontal !isHorizontal && isFieldset ? 'pt-0' : '', // If not horizontal and not a legend, we add 'd-block' class to label // so that label-align works !isHorizontal && !isFieldset ? 'd-block' : '', labelSize ? "col-form-label-".concat(labelSize) : '', this.labelAlignClasses, this.labelClass] }, [labelContent]); } } var $invalidFeedback = h(); var invalidFeedbackContent = normalizeSlot(SLOT_NAME_INVALID_FEEDBACK) || this.invalidFeedback; var invalidFeedbackId = invalidFeedbackContent ? safeId('_BV_feedback_invalid_') : null; if (invalidFeedbackContent) { $invalidFeedback = h(BFormInvalidFeedback, { props: { ariaLive: feedbackAriaLive, id: invalidFeedbackId, // If state is explicitly `false`, always show the feedback state: state, tooltip: tooltip }, attrs: { tabindex: invalidFeedbackContent ? '-1' : null } }, [invalidFeedbackContent]); } var $validFeedback = h(); var validFeedbackContent = normalizeSlot(SLOT_NAME_VALID_FEEDBACK) || this.validFeedback; var validFeedbackId = validFeedbackContent ? safeId('_BV_feedback_valid_') : null; if (validFeedbackContent) { $validFeedback = h(BFormValidFeedback, { props: { ariaLive: feedbackAriaLive, id: validFeedbackId, // If state is explicitly `true`, always show the feedback state: state, tooltip: tooltip }, attrs: { tabindex: validFeedbackContent ? '-1' : null } }, [validFeedbackContent]); } var $description = h(); var descriptionContent = normalizeSlot(SLOT_NAME_DESCRIPTION) || this.description; var descriptionId = descriptionContent ? safeId('_BV_description_') : null; if (descriptionContent) { $description = h(BFormText, { attrs: { id: descriptionId, tabindex: '-1' } }, [descriptionContent]); } // Update `ariaDescribedby` // Screen readers will read out any content linked to by `aria-describedby` // even if the content is hidden with `display: none;`, hence we only include // feedback IDs if the form group's state is explicitly valid or invalid var ariaDescribedby = this.ariaDescribedby = [descriptionId, state === false ? invalidFeedbackId : null, state === true ? validFeedbackId : null].filter(identity).join(' ') || null; var $content = h(isHorizontal ? BCol : 'div', { props: isHorizontal ? this.contentColProps : {}, ref: 'content' }, [normalizeSlot(SLOT_NAME_DEFAULT, { ariaDescribedby: ariaDescribedby, descriptionId: descriptionId, id: id, labelId: labelId }) || h(), $invalidFeedback, $validFeedback, $description]); // Return it wrapped in a form group // Note: Fieldsets do not support adding `row` or `form-row` directly // to them due to browser specific render issues, so we move the `form-row` // to an inner wrapper div when horizontal and using a fieldset return h(isFieldset ? 'fieldset' : isHorizontal ? BFormRow : 'div', { staticClass: 'form-group', class: [{ 'was-validated': this.validated }, this.stateClass], attrs: { id: id, disabled: isFieldset ? this.disabled : null, role: isFieldset ? null : 'group', 'aria-invalid': this.computedAriaInvalid, // Only apply `aria-labelledby` if we are a horizontal fieldset // as the legend is no longer a direct child of fieldset 'aria-labelledby': isFieldset && isHorizontal ? labelId : null } }, isHorizontal && isFieldset ? [h(BFormRow, [$label, $content])] : [$label, $content]); } };