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

559 lines (509 loc) 21.3 kB
var _watch; 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 { extend } from '../../vue'; import { NAME_FORM_SPINBUTTON } from '../../constants/components'; import { EVENT_NAME_CHANGE } from '../../constants/events'; import { PROP_TYPE_ARRAY_STRING, PROP_TYPE_BOOLEAN, PROP_TYPE_BOOLEAN_NUMBER, PROP_TYPE_FUNCTION, PROP_TYPE_NUMBER_STRING, PROP_TYPE_STRING } from '../../constants/props'; import { CODE_DOWN, CODE_END, CODE_HOME, CODE_PAGEUP, CODE_UP, CODE_PAGEDOWN } from '../../constants/key-codes'; import { SLOT_NAME_DECREMENT, SLOT_NAME_INCREMENT } from '../../constants/slots'; import { arrayIncludes, concat } from '../../utils/array'; import { attemptBlur, attemptFocus } from '../../utils/dom'; import { eventOnOff, stopEvent } from '../../utils/events'; import { identity } from '../../utils/identity'; import { isNull } from '../../utils/inspect'; import { isLocaleRTL } from '../../utils/locale'; import { mathFloor, mathMax, mathPow, mathRound } from '../../utils/math'; import { makeModelMixin } from '../../utils/model'; import { toFloat, toInteger } from '../../utils/number'; import { omit, sortKeys } from '../../utils/object'; import { hasPropFunction, makeProp, makePropsConfigurable } from '../../utils/props'; import { toString } from '../../utils/string'; import { attrsMixin } from '../../mixins/attrs'; import { formSizeMixin, props as formSizeProps } from '../../mixins/form-size'; import { formStateMixin, props as formStateProps } from '../../mixins/form-state'; import { idMixin, props as idProps } from '../../mixins/id'; import { normalizeSlotMixin } from '../../mixins/normalize-slot'; import { props as formControlProps } from '../../mixins/form-control'; import { BIconPlus, BIconDash } from '../../icons/icons'; // --- Constants --- var _makeModelMixin = makeModelMixin('value', { // Should this really be String, to match native number inputs? type: PROP_TYPE_BOOLEAN_NUMBER }), modelMixin = _makeModelMixin.mixin, modelProps = _makeModelMixin.props, MODEL_PROP_NAME = _makeModelMixin.prop, MODEL_EVENT_NAME = _makeModelMixin.event; // Default for spin button range and step var DEFAULT_MIN = 1; var DEFAULT_MAX = 100; var DEFAULT_STEP = 1; // Delay before auto-repeat in ms var DEFAULT_REPEAT_DELAY = 500; // Repeat interval in ms var DEFAULT_REPEAT_INTERVAL = 100; // Repeat rate increased after number of repeats var DEFAULT_REPEAT_THRESHOLD = 10; // Repeat speed multiplier (step multiplier, must be an integer) var DEFAULT_REPEAT_MULTIPLIER = 4; var KEY_CODES = [CODE_UP, CODE_DOWN, CODE_HOME, CODE_END, CODE_PAGEUP, CODE_PAGEDOWN]; // --- Props --- export var props = makePropsConfigurable(sortKeys(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread(_objectSpread({}, idProps), modelProps), omit(formControlProps, ['required', 'autofocus'])), formSizeProps), formStateProps), {}, { ariaControls: makeProp(PROP_TYPE_STRING), ariaLabel: makeProp(PROP_TYPE_STRING), formatterFn: makeProp(PROP_TYPE_FUNCTION), inline: makeProp(PROP_TYPE_BOOLEAN, false), labelDecrement: makeProp(PROP_TYPE_STRING, 'Decrement'), labelIncrement: makeProp(PROP_TYPE_STRING, 'Increment'), locale: makeProp(PROP_TYPE_ARRAY_STRING), max: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_MAX), min: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_MIN), placeholder: makeProp(PROP_TYPE_STRING), readonly: makeProp(PROP_TYPE_BOOLEAN, false), repeatDelay: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_REPEAT_DELAY), repeatInterval: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_REPEAT_INTERVAL), repeatStepMultiplier: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_REPEAT_MULTIPLIER), repeatThreshold: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_REPEAT_THRESHOLD), step: makeProp(PROP_TYPE_NUMBER_STRING, DEFAULT_STEP), vertical: makeProp(PROP_TYPE_BOOLEAN, false), wrap: makeProp(PROP_TYPE_BOOLEAN, false) })), NAME_FORM_SPINBUTTON); // --- Main Component --- // @vue/component export var BFormSpinbutton = /*#__PURE__*/extend({ name: NAME_FORM_SPINBUTTON, // Mixin order is important! mixins: [attrsMixin, idMixin, modelMixin, formSizeMixin, formStateMixin, normalizeSlotMixin], inheritAttrs: false, props: props, data: function data() { return { localValue: toFloat(this[MODEL_PROP_NAME], null), hasFocus: false }; }, computed: { required: function required() { return false; }, spinId: function spinId() { return this.safeId(); }, computedInline: function computedInline() { return this.inline && !this.vertical; }, computedReadonly: function computedReadonly() { return this.readonly && !this.disabled; }, computedRequired: function computedRequired() { return this.required && !this.computedReadonly && !this.disabled; }, computedStep: function computedStep() { return toFloat(this.step, DEFAULT_STEP); }, computedMin: function computedMin() { return toFloat(this.min, DEFAULT_MIN); }, computedMax: function computedMax() { // We round down to the nearest maximum step value var max = toFloat(this.max, DEFAULT_MAX); var step = this.computedStep; var min = this.computedMin; return mathFloor((max - min) / step) * step + min; }, computedDelay: function computedDelay() { var delay = toInteger(this.repeatDelay, 0); return delay > 0 ? delay : DEFAULT_REPEAT_DELAY; }, computedInterval: function computedInterval() { var interval = toInteger(this.repeatInterval, 0); return interval > 0 ? interval : DEFAULT_REPEAT_INTERVAL; }, computedThreshold: function computedThreshold() { return mathMax(toInteger(this.repeatThreshold, DEFAULT_REPEAT_THRESHOLD), 1); }, computedStepMultiplier: function computedStepMultiplier() { return mathMax(toInteger(this.repeatStepMultiplier, DEFAULT_REPEAT_MULTIPLIER), 1); }, computedPrecision: function computedPrecision() { // Quick and dirty way to get the number of decimals var step = this.computedStep; return mathFloor(step) === step ? 0 : (step.toString().split('.')[1] || '').length; }, computedMultiplier: function computedMultiplier() { return mathPow(10, this.computedPrecision || 0); }, valueAsFixed: function valueAsFixed() { var value = this.localValue; return isNull(value) ? '' : value.toFixed(this.computedPrecision); }, computedLocale: function computedLocale() { var locales = concat(this.locale).filter(identity); var nf = new Intl.NumberFormat(locales); return nf.resolvedOptions().locale; }, computedRTL: function computedRTL() { return isLocaleRTL(this.computedLocale); }, defaultFormatter: function defaultFormatter() { // Returns and `Intl.NumberFormat` formatter method reference var precision = this.computedPrecision; var nf = new Intl.NumberFormat(this.computedLocale, { style: 'decimal', useGrouping: false, minimumIntegerDigits: 1, minimumFractionDigits: precision, maximumFractionDigits: precision, notation: 'standard' }); // Return the format method reference return nf.format; }, computedFormatter: function computedFormatter() { var formatterFn = this.formatterFn; return hasPropFunction(formatterFn) ? formatterFn : this.defaultFormatter; }, computedAttrs: function computedAttrs() { return _objectSpread(_objectSpread({}, this.bvAttrs), {}, { role: 'group', lang: this.computedLocale, tabindex: this.disabled ? null : '-1', title: this.ariaLabel }); }, computedSpinAttrs: function computedSpinAttrs() { var spinId = this.spinId, value = this.localValue, required = this.computedRequired, disabled = this.disabled, state = this.state, computedFormatter = this.computedFormatter; var hasValue = !isNull(value); return _objectSpread(_objectSpread({ dir: this.computedRTL ? 'rtl' : 'ltr' }, this.bvAttrs), {}, { id: spinId, role: 'spinbutton', tabindex: disabled ? null : '0', 'aria-live': 'off', 'aria-label': this.ariaLabel || null, 'aria-controls': this.ariaControls || null, // TODO: May want to check if the value is in range 'aria-invalid': state === false || !hasValue && required ? 'true' : null, 'aria-required': required ? 'true' : null, // These attrs are required for role spinbutton 'aria-valuemin': toString(this.computedMin), 'aria-valuemax': toString(this.computedMax), // These should be `null` if the value is out of range // They must also be non-existent attrs if the value is out of range or `null` 'aria-valuenow': hasValue ? value : null, 'aria-valuetext': hasValue ? computedFormatter(value) : null }); } }, watch: (_watch = {}, _defineProperty(_watch, MODEL_PROP_NAME, function (value) { this.localValue = toFloat(value, null); }), _defineProperty(_watch, "localValue", function localValue(value) { this.$emit(MODEL_EVENT_NAME, value); }), _defineProperty(_watch, "disabled", function disabled(_disabled) { if (_disabled) { this.clearRepeat(); } }), _defineProperty(_watch, "readonly", function readonly(_readonly) { if (_readonly) { this.clearRepeat(); } }), _watch), created: function created() { // Create non reactive properties this.$_autoDelayTimer = null; this.$_autoRepeatTimer = null; this.$_keyIsDown = false; }, beforeDestroy: function beforeDestroy() { this.clearRepeat(); }, /* istanbul ignore next */ deactivated: function deactivated() { this.clearRepeat(); }, methods: { // --- Public methods --- focus: function focus() { if (!this.disabled) { attemptFocus(this.$refs.spinner); } }, blur: function blur() { if (!this.disabled) { attemptBlur(this.$refs.spinner); } }, // --- Private methods --- emitChange: function emitChange() { this.$emit(EVENT_NAME_CHANGE, this.localValue); }, stepValue: function stepValue(direction) { // Sets a new incremented or decremented value, supporting optional wrapping // Direction is either +1 or -1 (or a multiple thereof) var value = this.localValue; if (!this.disabled && !isNull(value)) { var step = this.computedStep * direction; var min = this.computedMin; var max = this.computedMax; var multiplier = this.computedMultiplier; var wrap = this.wrap; // We ensure that the value steps like a native input value = mathRound((value - min) / step) * step + min + step; // We ensure that precision is maintained (decimals) value = mathRound(value * multiplier) / multiplier; // Handle if wrapping is enabled this.localValue = value > max ? wrap ? min : max : value < min ? wrap ? max : min : value; } }, onFocusBlur: function onFocusBlur(event) { this.hasFocus = this.disabled ? false : event.type === 'focus'; }, stepUp: function stepUp() { var multiplier = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; var value = this.localValue; if (isNull(value)) { this.localValue = this.computedMin; } else { this.stepValue(+1 * multiplier); } }, stepDown: function stepDown() { var multiplier = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1; var value = this.localValue; if (isNull(value)) { this.localValue = this.wrap ? this.computedMax : this.computedMin; } else { this.stepValue(-1 * multiplier); } }, onKeydown: function onKeydown(event) { var keyCode = event.keyCode, altKey = event.altKey, ctrlKey = event.ctrlKey, metaKey = event.metaKey; /* istanbul ignore if */ if (this.disabled || this.readonly || altKey || ctrlKey || metaKey) { return; } if (arrayIncludes(KEY_CODES, keyCode)) { // https://w3c.github.io/aria-practices/#spinbutton stopEvent(event, { propagation: false }); /* istanbul ignore if */ if (this.$_keyIsDown) { // Keypress is already in progress return; } this.resetTimers(); if (arrayIncludes([CODE_UP, CODE_DOWN], keyCode)) { // The following use the custom auto-repeat handling this.$_keyIsDown = true; if (keyCode === CODE_UP) { this.handleStepRepeat(event, this.stepUp); } else if (keyCode === CODE_DOWN) { this.handleStepRepeat(event, this.stepDown); } } else { // These use native OS key repeating if (keyCode === CODE_PAGEUP) { this.stepUp(this.computedStepMultiplier); } else if (keyCode === CODE_PAGEDOWN) { this.stepDown(this.computedStepMultiplier); } else if (keyCode === CODE_HOME) { this.localValue = this.computedMin; } else if (keyCode === CODE_END) { this.localValue = this.computedMax; } } } }, onKeyup: function onKeyup(event) { // Emit a change event when the keyup happens var keyCode = event.keyCode, altKey = event.altKey, ctrlKey = event.ctrlKey, metaKey = event.metaKey; /* istanbul ignore if */ if (this.disabled || this.readonly || altKey || ctrlKey || metaKey) { return; } if (arrayIncludes(KEY_CODES, keyCode)) { stopEvent(event, { propagation: false }); this.resetTimers(); this.$_keyIsDown = false; this.emitChange(); } }, handleStepRepeat: function handleStepRepeat(event, stepper) { var _this = this; var _ref = event || {}, type = _ref.type, button = _ref.button; if (!this.disabled && !this.readonly) { /* istanbul ignore if */ if (type === 'mousedown' && button) { // We only respond to left (main === 0) button clicks return; } this.resetTimers(); // Step the counter initially stepper(1); var threshold = this.computedThreshold; var multiplier = this.computedStepMultiplier; var delay = this.computedDelay; var interval = this.computedInterval; // Initiate the delay/repeat interval this.$_autoDelayTimer = setTimeout(function () { var count = 0; _this.$_autoRepeatTimer = setInterval(function () { // After N initial repeats, we increase the incrementing step amount // We do this to minimize screen reader announcements of the value // (values are announced every change, which can be chatty for SR users) // And to make it easer to select a value when the range is large stepper(count < threshold ? 1 : multiplier); count++; }, interval); }, delay); } }, onMouseup: function onMouseup(event) { // `<body>` listener, only enabled when mousedown starts var _ref2 = event || {}, type = _ref2.type, button = _ref2.button; /* istanbul ignore if */ if (type === 'mouseup' && button) { // Ignore non left button (main === 0) mouse button click return; } stopEvent(event, { propagation: false }); this.resetTimers(); this.setMouseup(false); // Trigger the change event this.emitChange(); }, setMouseup: function setMouseup(on) { // Enable or disabled the body mouseup/touchend handlers // Use try/catch to handle case when called server side try { eventOnOff(on, document.body, 'mouseup', this.onMouseup, false); eventOnOff(on, document.body, 'touchend', this.onMouseup, false); } catch (_unused) {} }, resetTimers: function resetTimers() { clearTimeout(this.$_autoDelayTimer); clearInterval(this.$_autoRepeatTimer); this.$_autoDelayTimer = null; this.$_autoRepeatTimer = null; }, clearRepeat: function clearRepeat() { this.resetTimers(); this.setMouseup(false); this.$_keyIsDown = false; } }, render: function render(h) { var _this2 = this; var spinId = this.spinId, value = this.localValue, inline = this.computedInline, readonly = this.computedReadonly, vertical = this.vertical, disabled = this.disabled, computedFormatter = this.computedFormatter; var hasValue = !isNull(value); var makeButton = function makeButton(stepper, label, IconCmp, keyRef, shortcut, btnDisabled, slotName) { var $icon = h(IconCmp, { props: { scale: _this2.hasFocus ? 1.5 : 1.25 }, attrs: { 'aria-hidden': 'true' } }); var scope = { hasFocus: _this2.hasFocus }; var handler = function handler(event) { if (!disabled && !readonly) { stopEvent(event, { propagation: false }); _this2.setMouseup(true); // Since we `preventDefault()`, we must manually focus the button attemptFocus(event.currentTarget); _this2.handleStepRepeat(event, stepper); } }; return h('button', { staticClass: 'btn btn-sm border-0 rounded-0', class: { 'py-0': !vertical }, attrs: { tabindex: '-1', type: 'button', disabled: disabled || readonly || btnDisabled, 'aria-disabled': disabled || readonly || btnDisabled ? 'true' : null, 'aria-controls': spinId, 'aria-label': label || null, 'aria-keyshortcuts': shortcut || null }, on: { mousedown: handler, touchstart: handler }, key: keyRef || null, ref: keyRef }, [_this2.normalizeSlot(slotName, scope) || $icon]); }; // TODO: Add button disabled state when `wrap` is `false` and at value max/min var $increment = makeButton(this.stepUp, this.labelIncrement, BIconPlus, 'inc', 'ArrowUp', false, SLOT_NAME_INCREMENT); var $decrement = makeButton(this.stepDown, this.labelDecrement, BIconDash, 'dec', 'ArrowDown', false, SLOT_NAME_DECREMENT); var $hidden = h(); if (this.name && !disabled) { $hidden = h('input', { attrs: { type: 'hidden', name: this.name, form: this.form || null, // TODO: Should this be set to '' if value is out of range? value: this.valueAsFixed }, key: 'hidden' }); } var $spin = h( // We use 'output' element to make this accept a `<label for="id">` (Except IE) 'output', { staticClass: 'flex-grow-1', class: { 'd-flex': vertical, 'align-self-center': !vertical, 'align-items-center': vertical, 'border-top': vertical, 'border-bottom': vertical, 'border-left': !vertical, 'border-right': !vertical }, attrs: this.computedSpinAttrs, key: 'output', ref: 'spinner' }, [h('bdi', hasValue ? computedFormatter(value) : this.placeholder || '')]); return h('div', { staticClass: 'b-form-spinbutton form-control', class: [{ disabled: disabled, readonly: readonly, focus: this.hasFocus, 'd-inline-flex': inline || vertical, 'd-flex': !inline && !vertical, 'align-items-stretch': !vertical, 'flex-column': vertical }, this.sizeFormClass, this.stateClass], attrs: this.computedAttrs, on: { keydown: this.onKeydown, keyup: this.onKeyup, // We use capture phase (`!` prefix) since focus and blur do not bubble '!focus': this.onFocusBlur, '!blur': this.onFocusBlur } }, vertical ? [$increment, $hidden, $spin, $decrement] : [$decrement, $hidden, $spin, $increment]); } });