UNPKG

bootstrap-vue

Version:

BootstrapVue, with over 40 plugins and more than 80 custom components, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-ARIA accessibility markup.

510 lines (445 loc) 15.1 kB
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(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(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 Popper from 'popper.js'; import KeyCodes from '../utils/key-codes'; import warn from '../utils/warn'; import { BvEvent } from '../utils/bv-event.class'; import { closest, contains, isVisible, requestAF, selectAll } from '../utils/dom'; import { hasTouchSupport } from '../utils/env'; import { isNull } from '../utils/inspect'; import clickOutMixin from './click-out'; import focusInMixin from './focus-in'; import idMixin from './id'; // Return an array of visible items var filterVisibles = function filterVisibles(els) { return (els || []).filter(isVisible); }; // Root dropdown event names var ROOT_DROPDOWN_PREFIX = 'bv::dropdown::'; var ROOT_DROPDOWN_SHOWN = "".concat(ROOT_DROPDOWN_PREFIX, "shown"); var ROOT_DROPDOWN_HIDDEN = "".concat(ROOT_DROPDOWN_PREFIX, "hidden"); // Delay when loosing focus before closing menu (in ms) var FOCUSOUT_DELAY = hasTouchSupport ? 450 : 150; // Dropdown item CSS selectors var Selector = { FORM_CHILD: '.dropdown form', ITEM_SELECTOR: ['.dropdown-item', '.b-dropdown-form'].map(function (selector) { return "".concat(selector, ":not(.disabled):not([disabled])"); }).join(', ') }; // Popper attachment positions var AttachmentMap = { // Dropup left align TOP: 'top-start', // Dropup right align TOPEND: 'top-end', // Dropdown left align BOTTOM: 'bottom-start', // Dropdown right align BOTTOMEND: 'bottom-end', // Dropright left align RIGHT: 'right-start', // Dropright right align RIGHTEND: 'right-end', // Dropleft left align LEFT: 'left-start', // Dropleft right align LEFTEND: 'left-end' }; // @vue/component export default { mixins: [idMixin, clickOutMixin, focusInMixin], provide: function provide() { return { bvDropdown: this }; }, props: { disabled: { type: Boolean, default: false }, text: { // Button label type: String, default: '' }, html: { // Button label type: String }, dropup: { // place on top if possible type: Boolean, default: false }, dropright: { // place right if possible type: Boolean, default: false }, dropleft: { // place left if possible type: Boolean, default: false }, right: { // Right align menu (default is left align) type: Boolean, default: false }, offset: { // Number of pixels to offset menu, or a CSS unit value (i.e. 1px, 1rem, etc) type: [Number, String], default: 0 }, noFlip: { // Disable auto-flipping of menu from bottom<=>top type: Boolean, default: false }, lazy: { // If true, only render menu contents when open type: Boolean, default: false }, popperOpts: { // type: Object, default: function _default() {} } }, data: function data() { return { visible: false, inNavbar: null, visibleChangePrevented: false }; }, computed: { toggler: function toggler() { var toggle = this.$refs.toggle; return toggle ? toggle.$el || toggle : null; }, directionClass: function directionClass() { if (this.dropup) { return 'dropup'; } else if (this.dropright) { return 'dropright'; } else if (this.dropleft) { return 'dropleft'; } return ''; } }, watch: { visible: function visible(newValue, oldValue) { if (this.visibleChangePrevented) { this.visibleChangePrevented = false; return; } if (newValue !== oldValue) { var evtName = newValue ? 'show' : 'hide'; var bvEvt = new BvEvent(evtName, { cancelable: true, vueTarget: this, target: this.$refs.menu, relatedTarget: null, componentId: this.safeId ? this.safeId() : this.id || null }); this.emitEvent(bvEvt); if (bvEvt.defaultPrevented) { // Reset value and exit if canceled this.visibleChangePrevented = true; this.visible = oldValue; // Just in case a child element triggered this.hide(true) this.$off('hidden', this.focusToggler); return; } if (evtName === 'show') { this.showMenu(); } else { this.hideMenu(); } } }, disabled: function disabled(newValue, oldValue) { if (newValue !== oldValue && newValue && this.visible) { // Hide dropdown if disabled changes to true this.visible = false; } } }, created: function created() { // Create non-reactive property this.$_popper = null; this.$_hideTimeout = null; this.$_noop = function () {}; }, deactivated: function deactivated() /* istanbul ignore next: not easy to test */ { // In case we are inside a `<keep-alive>` this.visible = false; this.whileOpenListen(false); this.destroyPopper(); }, beforeDestroy: function beforeDestroy() { this.visible = false; this.whileOpenListen(false); this.destroyPopper(); this.clearHideTimeout(); }, methods: { // Event emitter emitEvent: function emitEvent(bvEvt) { var type = bvEvt.type; this.$emit(type, bvEvt); this.$root.$emit("".concat(ROOT_DROPDOWN_PREFIX).concat(type), bvEvt); }, showMenu: function showMenu() { var _this = this; if (this.disabled) { /* istanbul ignore next */ return; } // Are we in a navbar ? if (isNull(this.inNavbar) && this.isNav) { // We should use an injection for this /* istanbul ignore next */ this.inNavbar = Boolean(closest('.navbar', this.$el)); } // Disable totally Popper.js for Dropdown in Navbar if (!this.inNavbar) { if (typeof Popper === 'undefined') { /* istanbul ignore next */ warn('b-dropdown: Popper.js not found. Falling back to CSS positioning.'); } else { // for dropup with alignment we use the parent element as popper container var element = this.dropup && this.right || this.split ? this.$el : this.$refs.toggle; // Make sure we have a reference to an element, not a component! element = element.$el || element; // Instantiate popper.js this.createPopper(element); } } // Ensure other menus are closed this.$root.$emit(ROOT_DROPDOWN_SHOWN, this); this.whileOpenListen(true); // Wrap in nextTick to ensure menu is fully rendered/shown this.$nextTick(function () { // Focus on the menu container on show _this.focusMenu(); // Emit the shown event _this.$emit('shown'); }); }, hideMenu: function hideMenu() { this.whileOpenListen(false); this.$root.$emit(ROOT_DROPDOWN_HIDDEN, this); this.$emit('hidden'); this.destroyPopper(); }, createPopper: function createPopper(element) { this.destroyPopper(); this.$_popper = new Popper(element, this.$refs.menu, this.getPopperConfig()); }, destroyPopper: function destroyPopper() { if (this.$_popper) { // Ensure popper event listeners are removed cleanly this.$_popper.destroy(); } this.$_popper = null; }, clearHideTimeout: function clearHideTimeout() { /* istanbul ignore next */ if (this.$_hideTimeout) { clearTimeout(this.$_hideTimeout); this.$_hideTimeout = null; } }, getPopperConfig: function getPopperConfig() { var placement = AttachmentMap.BOTTOM; if (this.dropup) { placement = this.right ? AttachmentMap.TOPEND : AttachmentMap.TOP; } else if (this.dropright) { placement = AttachmentMap.RIGHT; } else if (this.dropleft) { placement = AttachmentMap.LEFT; } else if (this.right) { placement = AttachmentMap.BOTTOMEND; } var popperConfig = { placement: placement, modifiers: { offset: { offset: this.offset || 0 }, flip: { enabled: !this.noFlip } } }; if (this.boundary) { popperConfig.modifiers.preventOverflow = { boundariesElement: this.boundary }; } return _objectSpread({}, popperConfig, {}, this.popperOpts || {}); }, // Turn listeners on/off while open whileOpenListen: function whileOpenListen(isOpen) { // Hide the dropdown when clicked outside this.listenForClickOut = isOpen; // Hide the dropdown when it loses focus this.listenForFocusIn = isOpen; // Hide the dropdown when another dropdown is opened var method = isOpen ? '$on' : '$off'; this.$root[method](ROOT_DROPDOWN_SHOWN, this.rootCloseListener); }, rootCloseListener: function rootCloseListener(vm) { if (vm !== this) { this.visible = false; } }, show: function show() { var _this2 = this; // Public method to show dropdown if (this.disabled) { return; } // Wrap in a requestAnimationFrame to allow any previous // click handling to occur first requestAF(function () { _this2.visible = true; }); }, hide: function hide() { var refocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; // Public method to hide dropdown if (this.disabled) { /* istanbul ignore next */ return; } this.visible = false; if (refocus) { // Child element is closing the dropdown on click this.$once('hidden', this.focusToggler); } }, // Called only by a button that toggles the menu toggle: function toggle(evt) { evt = evt || {}; var type = evt.type; var key = evt.keyCode; if (type !== 'click' && !(type === 'keydown' && (key === KeyCodes.ENTER || key === KeyCodes.SPACE || key === KeyCodes.DOWN))) { // We only toggle on Click, Enter, Space, and Arrow Down /* istanbul ignore next */ return; } /* istanbul ignore next */ if (this.disabled) { this.visible = false; return; } this.$emit('toggle', evt); evt.preventDefault(); evt.stopPropagation(); // Toggle visibility if (this.visible) { this.hide(true); } else { this.show(); } }, // Called only in split button mode, for the split button click: function click(evt) { /* istanbul ignore next */ if (this.disabled) { this.visible = false; return; } this.$emit('click', evt); }, // Called from dropdown menu context onKeydown: function onKeydown(evt) { var key = evt.keyCode; if (key === KeyCodes.ESC) { // Close on ESC this.onEsc(evt); } else if (key === KeyCodes.DOWN) { // Down Arrow this.focusNext(evt, false); } else if (key === KeyCodes.UP) { // Up Arrow this.focusNext(evt, true); } }, // If uses presses ESC to close menu onEsc: function onEsc(evt) { if (this.visible) { this.visible = false; evt.preventDefault(); evt.stopPropagation(); // Return focus to original trigger button this.$once('hidden', this.focusToggler); } }, // Document click out listener clickOutHandler: function clickOutHandler(evt) { var _this3 = this; var target = evt.target; if (this.visible && !contains(this.$refs.menu, target) && !contains(this.toggler, target)) { var doHide = function doHide() { _this3.visible = false; return null; }; // When we are in a navbar (which has been responsively stacked), we // delay the dropdown's closing so that the next element has a chance // to have it's click handler fired (in case it's position moves on // the screen do to a navbar menu above it collapsing) // https://github.com/bootstrap-vue/bootstrap-vue/issues/4113 this.clearHideTimeout(); this.$_hideTimeout = this.inNavbar ? setTimeout(doHide, FOCUSOUT_DELAY) : doHide(); } }, // Document focusin listener focusInHandler: function focusInHandler(evt) { // Shared logic with click-out handler this.clickOutHandler(evt); }, // Keyboard nav focusNext: function focusNext(evt, up) { var _this4 = this; // Ignore key up/down on form elements if (!this.visible || evt && closest(Selector.FORM_CHILD, evt.target)) { /* istanbul ignore next: should never happen */ return; } evt.preventDefault(); evt.stopPropagation(); this.$nextTick(function () { var items = _this4.getItems(); if (items.length < 1) { /* istanbul ignore next: should never happen */ return; } var index = items.indexOf(evt.target); if (up && index > 0) { index--; } else if (!up && index < items.length - 1) { index++; } if (index < 0) { /* istanbul ignore next: should never happen */ index = 0; } _this4.focusItem(index, items); }); }, focusItem: function focusItem(idx, items) { var el = items.find(function (el, i) { return i === idx; }); if (el && el.focus) { el.focus(); } }, getItems: function getItems() { // Get all items return filterVisibles(selectAll(Selector.ITEM_SELECTOR, this.$refs.menu)); }, focusMenu: function focusMenu() { this.$refs.menu.focus && this.$refs.menu.focus(); }, focusToggler: function focusToggler() { var _this5 = this; this.$nextTick(function () { var toggler = _this5.toggler; if (toggler && toggler.focus) { toggler.focus(); } }); } } };