bootstrap-view
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
468 lines (455 loc) • 17.2 kB
JavaScript
function _typeof(o) { "@babel/helpers - typeof"; return _typeof = "function" == typeof Symbol && "symbol" == typeof Symbol.iterator ? function (o) { return typeof o; } : function (o) { return o && "function" == typeof Symbol && o.constructor === Symbol && o !== Symbol.prototype ? "symbol" : typeof o; }, _typeof(o); }
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; }
function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == _typeof(i) ? i : i + ""; }
function _toPrimitive(t, r) { if ("object" != _typeof(t) || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != _typeof(i)) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); }
import Popper from 'popper.js';
import { extend } from '../vue';
import { NAME_DROPDOWN } from '../constants/components';
import { HAS_TOUCH_SUPPORT } from '../constants/env';
import { EVENT_NAME_CLICK, EVENT_NAME_HIDDEN, EVENT_NAME_HIDE, EVENT_NAME_SHOW, EVENT_NAME_SHOWN, EVENT_NAME_TOGGLE } from '../constants/events';
import { CODE_DOWN, CODE_ENTER, CODE_ESC, CODE_SPACE, CODE_UP } from '../constants/key-codes';
import { PLACEMENT_TOP_START, PLACEMENT_TOP_END, PLACEMENT_BOTTOM_START, PLACEMENT_BOTTOM_END, PLACEMENT_RIGHT_START, PLACEMENT_LEFT_START } from '../constants/popper';
import { PROP_TYPE_BOOLEAN, PROP_TYPE_NUMBER_STRING, PROP_TYPE_OBJECT, PROP_TYPE_STRING } from '../constants/props';
import { HTMLElement } from '../constants/safe-types';
import { BvEvent } from '../utils/bv-event.class';
import { attemptFocus, closest, contains, isVisible, requestAF, selectAll } from '../utils/dom';
import { getRootEventName, stopEvent } from '../utils/events';
import { isNull } from '../utils/inspect';
import { mergeDeep, sortKeys } from '../utils/object';
import { makeProp, makePropsConfigurable } from '../utils/props';
import { warn } from '../utils/warn';
import { clickOutMixin } from './click-out';
import { focusInMixin } from './focus-in';
import { idMixin, props as idProps } from './id';
import { listenOnRootMixin } from './listen-on-root';
import { registerElementToInstance, removeElementToInstance } from '../utils/element-to-vue-instance-registry';
// --- Constants ---
var ROOT_EVENT_NAME_SHOWN = getRootEventName(NAME_DROPDOWN, EVENT_NAME_SHOWN);
var ROOT_EVENT_NAME_HIDDEN = getRootEventName(NAME_DROPDOWN, EVENT_NAME_HIDDEN);
// CSS selectors
var SELECTOR_FORM_CHILD = '.dropdown form';
var SELECTOR_ITEM = ['.dropdown-item', '.b-dropdown-form'].map(function (selector) {
return "".concat(selector, ":not(.disabled):not([disabled])");
}).join(', ');
// --- Helper methods ---
// Return an array of visible items
var filterVisibles = function filterVisibles(els) {
return (els || []).filter(isVisible);
};
// --- Props ---
export var props = makePropsConfigurable(sortKeys(_objectSpread(_objectSpread({}, idProps), {}, {
// String: `scrollParent`, `window` or `viewport`
// HTMLElement: HTML Element reference
boundary: makeProp([HTMLElement, PROP_TYPE_STRING], 'scrollParent'),
disabled: makeProp(PROP_TYPE_BOOLEAN, false),
// Place left if possible
dropleft: makeProp(PROP_TYPE_BOOLEAN, false),
// Place right if possible
dropright: makeProp(PROP_TYPE_BOOLEAN, false),
// Place on top if possible
dropup: makeProp(PROP_TYPE_BOOLEAN, false),
// Disable auto-flipping of menu from bottom <=> top
noFlip: makeProp(PROP_TYPE_BOOLEAN, false),
// Number of pixels or a CSS unit value to offset menu
// (i.e. `1px`, `1rem`, etc.)
offset: makeProp(PROP_TYPE_NUMBER_STRING, 0),
popperOpts: makeProp(PROP_TYPE_OBJECT, {}),
// Right align menu (default is left align)
right: makeProp(PROP_TYPE_BOOLEAN, false)
})), NAME_DROPDOWN);
// --- Mixin ---
// @vue/component
export var dropdownMixin = extend({
mixins: [idMixin, listenOnRootMixin, clickOutMixin, focusInMixin],
provide: function provide() {
var _this = this;
return {
getBvDropdown: function getBvDropdown() {
return _this;
}
};
},
inject: {
getBvNavbar: {
default: function _default() {
return function () {
return null;
};
}
}
},
props: props,
data: function data() {
return {
visible: false,
visibleChangePrevented: false
};
},
computed: {
bvNavbar: function bvNavbar() {
return this.getBvNavbar();
},
inNavbar: function inNavbar() {
return !isNull(this.bvNavbar);
},
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 '';
},
boundaryClass: function boundaryClass() {
// Position `static` is needed to allow menu to "breakout" of the `scrollParent`
// boundaries when boundary is anything other than `scrollParent`
// See: https://github.com/twbs/bootstrap/issues/24251#issuecomment-341413786
return this.boundary !== 'scrollParent' && !this.inNavbar ? 'position-static' : '';
},
hideDelay: function hideDelay() {
return this.inNavbar ? HAS_TOUCH_SUPPORT ? 300 : 50 : 0;
}
},
watch: {
visible: function visible(newValue, oldValue) {
if (this.visibleChangePrevented) {
this.visibleChangePrevented = false;
return;
}
if (newValue !== oldValue) {
var eventName = newValue ? EVENT_NAME_SHOW : EVENT_NAME_HIDE;
var bvEvent = new BvEvent(eventName, {
cancelable: true,
vueTarget: this,
target: this.$refs.menu,
relatedTarget: null,
componentId: this.safeId ? this.safeId() : this.id || null
});
this.emitEvent(bvEvent);
if (bvEvent.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(EVENT_NAME_HIDDEN, this.focusToggler);
return;
}
if (newValue) {
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 private non-reactive props
this.$_popper = null;
this.$_hideTimeout = null;
},
/* istanbul ignore next */deactivated: function deactivated() {
// In case we are inside a `<keep-alive>`
this.visible = false;
this.whileOpenListen(false);
this.destroyPopper();
},
mounted: function mounted() {
registerElementToInstance(this.$el, this);
},
beforeDestroy: function beforeDestroy() {
this.visible = false;
this.whileOpenListen(false);
this.destroyPopper();
this.clearHideTimeout();
removeElementToInstance(this.$el);
},
methods: {
// Event emitter
emitEvent: function emitEvent(bvEvent) {
var type = bvEvent.type;
this.emitOnRoot(getRootEventName(NAME_DROPDOWN, type), bvEvent);
this.$emit(type, bvEvent);
},
showMenu: function showMenu() {
var _this2 = this;
if (this.disabled) {
/* istanbul ignore next */
return;
}
// Only instantiate Popper.js when dropdown is not in `<b-navbar>`
if (!this.inNavbar) {
if (typeof Popper === 'undefined') {
/* istanbul ignore next */
warn('Popper.js not found. Falling back to CSS positioning', NAME_DROPDOWN);
} else {
// For dropup with alignment we use the parent element as popper container
var el = this.dropup && this.right || this.split ? this.$el : this.$refs.toggle;
// Make sure we have a reference to an element, not a component!
el = el.$el || el;
// Instantiate Popper.js
this.createPopper(el);
}
}
// Ensure other menus are closed
this.emitOnRoot(ROOT_EVENT_NAME_SHOWN, this);
// Enable listeners
this.whileOpenListen(true);
// Wrap in `$nextTick()` to ensure menu is fully rendered/shown
this.$nextTick(function () {
// Focus on the menu container on show
_this2.focusMenu();
// Emit the shown event
_this2.$emit(EVENT_NAME_SHOWN);
});
},
hideMenu: function hideMenu() {
this.whileOpenListen(false);
this.emitOnRoot(ROOT_EVENT_NAME_HIDDEN, this);
this.$emit(EVENT_NAME_HIDDEN);
this.destroyPopper();
},
createPopper: function createPopper(element) {
this.destroyPopper();
this.$_popper = new Popper(element, this.$refs.menu, this.getPopperConfig());
},
// Ensure popper event listeners are removed cleanly
destroyPopper: function destroyPopper() {
this.$_popper && this.$_popper.destroy();
this.$_popper = null;
},
// Instructs popper to re-computes the dropdown position
// useful if the content changes size
updatePopper: function updatePopper() {
try {
this.$_popper.scheduleUpdate();
} catch (_unused) {}
},
clearHideTimeout: function clearHideTimeout() {
clearTimeout(this.$_hideTimeout);
this.$_hideTimeout = null;
},
getPopperConfig: function getPopperConfig() {
var placement = PLACEMENT_BOTTOM_START;
if (this.dropup) {
placement = this.right ? PLACEMENT_TOP_END : PLACEMENT_TOP_START;
} else if (this.dropright) {
placement = PLACEMENT_RIGHT_START;
} else if (this.dropleft) {
placement = PLACEMENT_LEFT_START;
} else if (this.right) {
placement = PLACEMENT_BOTTOM_END;
}
var popperConfig = {
placement: placement,
modifiers: {
offset: {
offset: this.offset || 0
},
flip: {
enabled: !this.noFlip
}
}
};
var boundariesElement = this.boundary;
if (boundariesElement) {
popperConfig.modifiers.preventOverflow = {
boundariesElement: boundariesElement
};
}
return mergeDeep(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 ? 'listenOnRoot' : 'listenOffRoot';
this[method](ROOT_EVENT_NAME_SHOWN, this.rootCloseListener);
},
rootCloseListener: function rootCloseListener(vm) {
if (vm !== this) {
this.visible = false;
}
},
// Public method to show dropdown
show: function show() {
var _this3 = this;
if (this.disabled) {
return;
}
// Wrap in a `requestAF()` to allow any previous
// click handling to occur first
requestAF(function () {
_this3.visible = true;
});
},
// Public method to hide dropdown
hide: function hide() {
var refocus = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false;
/* istanbul ignore next */
if (this.disabled) {
return;
}
this.visible = false;
if (refocus) {
// Child element is closing the dropdown on click
this.$once(EVENT_NAME_HIDDEN, this.focusToggler);
}
},
// Called only by a button that toggles the menu
toggle: function toggle(event) {
event = event || {};
// Early exit when not a click event or ENTER, SPACE or DOWN were pressed
var _event = event,
type = _event.type,
keyCode = _event.keyCode;
if (type !== 'click' && !(type === 'keydown' && [CODE_ENTER, CODE_SPACE, CODE_DOWN].indexOf(keyCode) !== -1)) {
/* istanbul ignore next */
return;
}
/* istanbul ignore next */
if (this.disabled) {
this.visible = false;
return;
}
this.$emit(EVENT_NAME_TOGGLE, event);
stopEvent(event);
// Toggle visibility
if (this.visible) {
this.hide(true);
} else {
this.show();
}
},
// Mousedown handler for the toggle
/* istanbul ignore next */
onMousedown: function onMousedown(event) {
// We prevent the 'mousedown' event for the toggle to stop the
// 'focusin' event from being fired
// The event would otherwise be picked up by the global 'focusin'
// listener and there is no cross-browser solution to detect it
// relates to the toggle click
// The 'click' event will still be fired and we handle closing
// other dropdowns there too
// See https://github.com/bootstrap-vue/bootstrap-vue/issues/4328
stopEvent(event, {
propagation: false
});
},
// Called from dropdown menu context
onKeydown: function onKeydown(event) {
var keyCode = event.keyCode;
if (keyCode === CODE_ESC) {
// Close on ESC
this.onEsc(event);
} else if (keyCode === CODE_DOWN) {
// Down Arrow
this.focusNext(event, false);
} else if (keyCode === CODE_UP) {
// Up Arrow
this.focusNext(event, true);
}
},
// If user presses ESC, close the menu
onEsc: function onEsc(event) {
if (this.visible) {
this.visible = false;
stopEvent(event);
// Return focus to original trigger button
this.$once(EVENT_NAME_HIDDEN, this.focusToggler);
}
},
// Called only in split button mode, for the split button
onSplitClick: function onSplitClick(event) {
/* istanbul ignore next */
if (this.disabled) {
this.visible = false;
return;
}
this.$emit(EVENT_NAME_CLICK, event);
},
// Shared hide handler between click-out and focus-in events
hideHandler: function hideHandler(event) {
var _this4 = this;
var target = event.target;
if (this.visible && !contains(this.$refs.menu, target) && !contains(this.toggler, target)) {
this.clearHideTimeout();
this.$_hideTimeout = setTimeout(function () {
return _this4.hide();
}, this.hideDelay);
}
},
// Document click-out listener
clickOutHandler: function clickOutHandler(event) {
this.hideHandler(event);
},
// Document focus-in listener
focusInHandler: function focusInHandler(event) {
this.hideHandler(event);
},
// Keyboard nav
focusNext: function focusNext(event, up) {
var _this5 = this;
// Ignore key up/down on form elements
var target = event.target;
if (!this.visible || event && closest(SELECTOR_FORM_CHILD, target)) {
/* istanbul ignore next: should never happen */
return;
}
stopEvent(event);
this.$nextTick(function () {
var items = _this5.getItems();
if (items.length < 1) {
/* istanbul ignore next: should never happen */
return;
}
var index = items.indexOf(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;
}
_this5.focusItem(index, items);
});
},
focusItem: function focusItem(index, items) {
var el = items.find(function (el, i) {
return i === index;
});
attemptFocus(el);
},
getItems: function getItems() {
// Get all items
return filterVisibles(selectAll(SELECTOR_ITEM, this.$refs.menu));
},
focusMenu: function focusMenu() {
attemptFocus(this.$refs.menu);
},
focusToggler: function focusToggler() {
var _this6 = this;
this.$nextTick(function () {
attemptFocus(_this6.toggler);
});
}
}
});