bootstrap-vue
Version:
BootstrapVue, with over 40 plugins and more than 80 custom components, custom directives, and over 300 icons, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-AR
439 lines (386 loc) • 16.3 kB
JavaScript
function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (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 = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { 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; }
// Utils
import memoize from '../../utils/memoize';
import { arrayIncludes } from '../../utils/array';
import { getBreakpointsUpCached } from '../../utils/config';
import { select, selectAll, isVisible, setAttr, removeAttr, getAttr } from '../../utils/dom';
import { isBrowser } from '../../utils/env';
import { isBoolean } from '../../utils/inspect';
import { keys, create } from '../../utils/object';
import { upperFirst } from '../../utils/string'; // Mixins
import formStateMixin from '../../mixins/form-state';
import idMixin from '../../mixins/id';
import normalizeSlotMixin from '../../mixins/normalize-slot'; // Sub components
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'; // Component name
var NAME = 'BFormGroup'; // Selector for finding first input in the form-group
var SELECTOR = 'input:not([disabled]),textarea:not([disabled]),select:not([disabled])'; // Render helper functions (here rather than polluting the instance with more methods)
var renderInvalidFeedback = function renderInvalidFeedback(h, ctx) {
var content = ctx.normalizeSlot('invalid-feedback') || ctx.invalidFeedback;
var invalidFeedback = h();
if (content) {
invalidFeedback = h(BFormInvalidFeedback, {
props: {
id: ctx.invalidFeedbackId,
// If state is explicitly false, always show the feedback
state: ctx.computedState,
tooltip: ctx.tooltip,
ariaLive: ctx.feedbackAriaLive,
role: ctx.feedbackAriaLive ? 'alert' : null
},
attrs: {
tabindex: content ? '-1' : null
}
}, [content]);
}
return invalidFeedback;
};
var renderValidFeedback = function renderValidFeedback(h, ctx) {
var content = ctx.normalizeSlot('valid-feedback') || ctx.validFeedback;
var validFeedback = h();
if (content) {
validFeedback = h(BFormValidFeedback, {
props: {
id: ctx.validFeedbackId,
// If state is explicitly true, always show the feedback
state: ctx.computedState,
tooltip: ctx.tooltip,
ariaLive: ctx.feedbackAriaLive,
role: ctx.feedbackAriaLive ? 'alert' : null
},
attrs: {
tabindex: content ? '-1' : null
}
}, [content]);
}
return validFeedback;
};
var renderHelpText = function renderHelpText(h, ctx) {
// Form help text (description)
var content = ctx.normalizeSlot('description') || ctx.description;
var description = h();
if (content) {
description = h(BFormText, {
attrs: {
id: ctx.descriptionId,
tabindex: content ? '-1' : null
}
}, [content]);
}
return description;
};
var renderLabel = function renderLabel(h, ctx) {
// Render label/legend inside b-col if necessary
var content = ctx.normalizeSlot('label') || ctx.label;
var labelFor = ctx.labelFor;
var isLegend = !labelFor;
var isHorizontal = ctx.isHorizontal;
var labelTag = isLegend ? 'legend' : 'label';
if (!content && !isHorizontal) {
return h();
} else if (ctx.labelSrOnly) {
var label = h();
if (content) {
label = h(labelTag, {
class: 'sr-only',
attrs: {
id: ctx.labelId,
for: labelFor || null
}
}, [content]);
}
return h(isHorizontal ? BCol : 'div', {
props: isHorizontal ? ctx.labelColProps : {}
}, [label]);
} else {
return h(isHorizontal ? BCol : labelTag, {
on: isLegend ? {
click: ctx.legendClick
} : {},
props: isHorizontal ? _objectSpread({
tag: labelTag
}, ctx.labelColProps) : {},
attrs: {
id: ctx.labelId,
for: labelFor || null,
// We add a tab index to legend so that screen readers
// will properly read the aria-labelledby in IE.
tabindex: isLegend ? '-1' : null
},
class: [// Hide the focus ring on the legend
isLegend ? 'bv-no-focus-ring' : '', // When horizontal or if a legend is rendered, add col-form-label
// 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 || isLegend ? 'col-form-label' : '', // Emulate label padding top of 0 on legend when not horizontal
!isHorizontal && isLegend ? 'pt-0' : '', // If not horizontal and not a legend, we add d-block to label
// so that label-align works
!isHorizontal && !isLegend ? 'd-block' : '', ctx.labelSize ? "col-form-label-".concat(ctx.labelSize) : '', ctx.labelAlignClasses, ctx.labelClass]
}, [content]);
}
}; // -- BFormGroup Prop factory -- used for lazy generation of props
// Memoize this function to return cached values to
// save time in computed functions
var makePropName = memoize(function () {
var breakpoint = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '';
var prefix = arguments.length > 1 ? arguments[1] : undefined;
return "".concat(prefix).concat(upperFirst(breakpoint));
}); // BFormGroup prop generator for lazy generation of props
var generateProps = function generateProps() {
var BREAKPOINTS = getBreakpointsUpCached(); // Generate the labelCol breakpoint props
var bpLabelColProps = BREAKPOINTS.reduce(function (props, breakpoint) {
// i.e. label-cols, label-cols-sm, label-cols-md, ...
props[makePropName(breakpoint, 'labelCols')] = {
type: [Number, String, Boolean],
default: breakpoint ? false : null
};
return props;
}, create(null)); // Generate the labelAlign breakpoint props
var bpLabelAlignProps = BREAKPOINTS.reduce(function (props, breakpoint) {
// label-align, label-align-sm, label-align-md, ...
props[makePropName(breakpoint, 'labelAlign')] = {
type: String,
// left, right, center
default: null
};
return props;
}, create(null));
return _objectSpread({
label: {
type: String,
default: null
},
labelFor: {
type: String,
default: null
},
labelSize: {
type: String,
default: null
},
labelSrOnly: {
type: Boolean,
default: false
}
}, bpLabelColProps, {}, bpLabelAlignProps, {
labelClass: {
type: [String, Array, Object],
default: null
},
description: {
type: String,
default: null
},
invalidFeedback: {
type: String,
default: null
},
validFeedback: {
type: String,
default: null
},
tooltip: {
// Enable tooltip style feedback
type: Boolean,
default: false
},
feedbackAriaLive: {
type: String,
default: 'assertive'
},
validated: {
type: Boolean,
default: false
},
disabled: {
type: Boolean,
default: false
}
});
}; // We do not use Vue.extend here as that would evaluate the props
// immediately, which we do not want to happen
// @vue/component
export var BFormGroup = {
name: NAME,
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();
},
computed: {
labelColProps: function labelColProps() {
var _this = this;
var props = {};
getBreakpointsUpCached().forEach(function (breakpoint) {
// Grab the value if the label column breakpoint prop
var propVal = _this[makePropName(breakpoint, 'labelCols')]; // Handle case where the prop's value is an empty string,
// which represents true
propVal = propVal === '' ? true : propVal || false;
if (!isBoolean(propVal) && propVal !== 'auto') {
// Convert to column size to number
propVal = parseInt(propVal, 10) || 0; // Ensure column size is greater than 0
propVal = propVal > 0 ? propVal : false;
}
if (propVal) {
// Add the prop to the list of props to give to b-col
// If breakpoint is '' (labelCols=true), then we use the
// col prop to make equal width at xs
var bColPropName = breakpoint || (isBoolean(propVal) ? 'col' : 'cols'); // Add it to the props
props[bColPropName] = propVal;
}
});
return props;
},
labelAlignClasses: function labelAlignClasses() {
var _this2 = this;
var classes = [];
getBreakpointsUpCached().forEach(function (breakpoint) {
// Assemble the label column breakpoint align classes
var propVal = _this2[makePropName(breakpoint, 'labelAlign')] || null;
if (propVal) {
var className = breakpoint ? "text-".concat(breakpoint, "-").concat(propVal) : "text-".concat(propVal);
classes.push(className);
}
});
return classes;
},
isHorizontal: function isHorizontal() {
// Determine if the resultant form-group will be rendered
// horizontal (meaning it has label-col breakpoints)
return keys(this.labelColProps).length > 0;
},
labelId: function labelId() {
return this.hasNormalizedSlot('label') || this.label ? this.safeId('_BV_label_') : null;
},
descriptionId: function descriptionId() {
return this.hasNormalizedSlot('description') || this.description ? this.safeId('_BV_description_') : null;
},
hasInvalidFeedback: function hasInvalidFeedback() {
// Used for computing aria-describedby
return this.computedState === false && (this.hasNormalizedSlot('invalid-feedback') || this.invalidFeedback);
},
invalidFeedbackId: function invalidFeedbackId() {
return this.hasInvalidFeedback ? this.safeId('_BV_feedback_invalid_') : null;
},
hasValidFeedback: function hasValidFeedback() {
// Used for computing aria-describedby
return this.computedState === true && (this.hasNormalizedSlot('valid-feedback') || this.validFeedback);
},
validFeedbackId: function validFeedbackId() {
return this.hasValidFeedback ? this.safeId('_BV_feedback_valid_') : null;
},
describedByIds: function describedByIds() {
// 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.
return [this.descriptionId, this.invalidFeedbackId, this.validFeedbackId].filter(Boolean).join(' ') || null;
}
},
watch: {
describedByIds: function describedByIds(add, remove) {
if (add !== remove) {
this.setInputDescribedBy(add, remove);
}
}
},
mounted: function mounted() {
var _this3 = this;
this.$nextTick(function () {
// Set the aria-describedby IDs on the input specified by label-for
// We do this in a nextTick to ensure the children have finished rendering
_this3.setInputDescribedBy(_this3.describedByIds);
});
},
methods: {
legendClick: function legendClick(evt) {
if (this.labelFor) {
// Don't do anything if labelFor is set
/* istanbul ignore next: clicking a label will focus the input, so no need to test */
return;
}
var tagName = evt.target ? evt.target.tagName : '';
if (/^(input|select|textarea|label|button|a)$/i.test(tagName)) {
// If clicked an interactive element inside legend,
// we just let the default happen
/* istanbul ignore next */
return;
}
var inputs = selectAll(SELECTOR, this.$refs.content).filter(isVisible);
if (inputs && inputs.length === 1 && inputs[0].focus) {
// if only a single input, focus it, emulating label behaviour
try {
inputs[0].focus();
} catch (_unused) {}
}
},
setInputDescribedBy: function setInputDescribedBy(add, remove) {
// Sets the `aria-describedby` attribute on the input if label-for 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.
if (this.labelFor && isBrowser) {
var input = select("#".concat(this.labelFor), this.$refs.content);
if (input) {
var adb = 'aria-describedby';
var ids = (getAttr(input, adb) || '').split(/\s+/);
add = (add || '').split(/\s+/);
remove = (remove || '').split(/\s+/); // Update ID list, preserving any original IDs
// and ensuring the ID's are unique
ids = ids.filter(function (id) {
return !arrayIncludes(remove, id);
}).concat(add).filter(Boolean);
ids = keys(ids.reduce(function (memo, id) {
return _objectSpread({}, memo, _defineProperty({}, id, true));
}, {})).join(' ').trim();
if (ids) {
setAttr(input, adb, ids);
} else {
// No IDs, so remove the attribute
removeAttr(input, adb);
}
}
}
}
},
render: function render(h) {
var isFieldset = !this.labelFor;
var isHorizontal = this.isHorizontal; // Generate the label
var label = renderLabel(h, this); // Generate the content
var content = h(isHorizontal ? BCol : 'div', {
ref: 'content',
// Hide focus ring
staticClass: 'bv-no-focus-ring',
attrs: {
tabindex: isFieldset ? '-1' : null,
role: isFieldset ? 'group' : null
}
}, [this.normalizeSlot('default') || h(), renderInvalidFeedback(h, this), renderValidFeedback(h, this), renderHelpText(h, this)]); // Create the form-group
var data = {
staticClass: 'form-group',
class: [this.validated ? 'was-validated' : null, this.stateClass],
attrs: {
id: this.safeId(),
disabled: isFieldset ? this.disabled : null,
role: isFieldset ? null : 'group',
'aria-invalid': this.computedState === false ? 'true' : null,
// 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 ? this.labelId : null,
// Only apply aria-describedby IDs if we are a fieldset
// as the input will have the IDs when not a fieldset
'aria-describedby': isFieldset ? this.describedByIds : null
}
}; // 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', data, isHorizontal && isFieldset ? [h(BFormRow, {}, [label, content])] : [label, content]);
}
};