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

772 lines (690 loc) 23.9 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(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; } import Vue from '../../vue'; import { NAME_TABS, NAME_TAB_BUTTON_HELPER } from '../../constants/components'; import { CODE_DOWN, CODE_END, CODE_HOME, CODE_LEFT, CODE_RIGHT, CODE_SPACE, CODE_UP } from '../../constants/key-codes'; import { SLOT_NAME_TITLE } from '../../constants/slot-names'; import identity from '../../utils/identity'; import looseEqual from '../../utils/loose-equal'; import observeDom from '../../utils/observe-dom'; import stableSort from '../../utils/stable-sort'; import { arrayIncludes, concat } from '../../utils/array'; import { BvEvent } from '../../utils/bv-event.class'; import { attemptFocus, requestAF, selectAll } from '../../utils/dom'; import { stopEvent } from '../../utils/events'; import { isEvent } from '../../utils/inspect'; import { mathMax } from '../../utils/math'; import { toInteger } from '../../utils/number'; import { omit } from '../../utils/object'; import idMixin from '../../mixins/id'; import normalizeSlotMixin from '../../mixins/normalize-slot'; import { BLink } from '../link/link'; import { BNav, props as BNavProps } from '../nav/nav'; // --- Constants --- var navProps = omit(BNavProps, ['tabs', 'isNavBar', 'cardHeader']); // --- Helper methods --- // Filter function to filter out disabled tabs var notDisabled = function notDisabled(tab) { return !tab.disabled; }; // --- Helper components --- // @vue/component var BVTabButton = /*#__PURE__*/Vue.extend({ name: NAME_TAB_BUTTON_HELPER, inject: { bvTabs: { /* istanbul ignore next */ default: function _default() /* istanbul ignore next */ { return {}; } } }, props: { // Reference to the child <b-tab> instance tab: { default: null }, tabs: { type: Array, /* istanbul ignore next */ default: function _default() /* istanbul ignore next */ { return []; } }, id: { type: String, default: null }, controls: { type: String, default: null }, tabIndex: { type: Number, default: null }, posInSet: { type: Number, default: null }, setSize: { type: Number, default: null }, noKeyNav: { type: Boolean, default: false } }, methods: { focus: function focus() { attemptFocus(this.$refs.link); }, handleEvt: function handleEvt(evt) { if (this.tab.disabled) { /* istanbul ignore next */ return; } var type = evt.type, keyCode = evt.keyCode, shiftKey = evt.shiftKey; if (type === 'click') { stopEvent(evt); this.$emit('click', evt); } else if (type === 'keydown' && keyCode === CODE_SPACE) { // For ARIA tabs the SPACE key will also trigger a click/select // Even with keyboard navigation disabled, SPACE should "click" the button // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/4323 stopEvent(evt); this.$emit('click', evt); } else if (type === 'keydown' && !this.noKeyNav) { // For keyboard navigation if ([CODE_UP, CODE_LEFT, CODE_HOME].indexOf(keyCode) !== -1) { stopEvent(evt); if (shiftKey || keyCode === CODE_HOME) { this.$emit('first', evt); } else { this.$emit('prev', evt); } } else if ([CODE_DOWN, CODE_RIGHT, CODE_END].indexOf(keyCode) !== -1) { stopEvent(evt); if (shiftKey || keyCode === CODE_END) { this.$emit('last', evt); } else { this.$emit('next', evt); } } } } }, render: function render(h) { var id = this.id, tabIndex = this.tabIndex, setSize = this.setSize, posInSet = this.posInSet, controls = this.controls, handleEvt = this.handleEvt; var _this$tab = this.tab, title = _this$tab.title, localActive = _this$tab.localActive, disabled = _this$tab.disabled, titleItemClass = _this$tab.titleItemClass, titleLinkClass = _this$tab.titleLinkClass, titleLinkAttributes = _this$tab.titleLinkAttributes; var $link = h(BLink, { ref: 'link', staticClass: 'nav-link', class: [{ active: localActive && !disabled, disabled: disabled }, titleLinkClass, // Apply <b-tabs> `activeNavItemClass` styles when the tab is active localActive ? this.bvTabs.activeNavItemClass : null], props: { disabled: disabled }, attrs: _objectSpread(_objectSpread({}, titleLinkAttributes), {}, { role: 'tab', id: id, // Roving tab index when keynav enabled tabindex: tabIndex, 'aria-selected': localActive && !disabled ? 'true' : 'false', 'aria-setsize': setSize, 'aria-posinset': posInSet, 'aria-controls': controls }), on: { click: handleEvt, keydown: handleEvt } }, [this.tab.normalizeSlot(SLOT_NAME_TITLE) || title]); return h('li', { staticClass: 'nav-item', class: [titleItemClass], attrs: { role: 'presentation' } }, [$link]); } }); // @vue/component export var BTabs = /*#__PURE__*/Vue.extend({ name: NAME_TABS, mixins: [idMixin, normalizeSlotMixin], provide: function provide() { return { bvTabs: this }; }, model: { prop: 'value', event: 'input' }, props: _objectSpread(_objectSpread({}, navProps), {}, { tag: { type: String, default: 'div' }, card: { type: Boolean, default: false }, end: { // Synonym for 'bottom' type: Boolean, default: false }, noFade: { type: Boolean, default: false }, noNavStyle: { type: Boolean, default: false }, noKeyNav: { type: Boolean, default: false }, lazy: { // This prop is sniffed by the <b-tab> child type: Boolean, default: false }, contentClass: { type: [String, Array, Object] // default: null }, navClass: { type: [String, Array, Object] // default: null }, navWrapperClass: { type: [String, Array, Object] // default: null }, activeNavItemClass: { // Only applied to the currently active <b-nav-item> type: [String, Array, Object] // default: null }, activeTabClass: { // Only applied to the currently active <b-tab> // This prop is sniffed by the <b-tab> child type: [String, Array, Object] // default: null }, value: { // v-model type: Number, default: null } }), data: function data() { return { // Index of current tab currentTab: toInteger(this.value, -1), // Array of direct child <b-tab> instances, in DOM order tabs: [], // Array of child instances registered (for triggering reactive updates) registeredTabs: [], // Flag to know if we are mounted or not isMounted: false }; }, computed: { fade: function fade() { // This computed prop is sniffed by the tab child return !this.noFade; }, localNavClass: function localNavClass() { var classes = []; if (this.card && this.vertical) { classes.push('card-header', 'h-100', 'border-bottom-0', 'rounded-0'); } return [].concat(classes, [this.navClass]); } }, watch: { currentTab: function currentTab(newVal) { var index = -1; // Ensure only one tab is active at most this.tabs.forEach(function (tab, idx) { if (newVal === idx && !tab.disabled) { tab.localActive = true; index = idx; } else { tab.localActive = false; } }); // Update the v-model this.$emit('input', index); }, value: function value(newVal, oldVal) { if (newVal !== oldVal) { newVal = toInteger(newVal, -1); oldVal = toInteger(oldVal, 0); var tabs = this.tabs; if (tabs[newVal] && !tabs[newVal].disabled) { this.activateTab(tabs[newVal]); } else { // Try next or prev tabs if (newVal < oldVal) { this.previousTab(); } else { this.nextTab(); } } } }, registeredTabs: function registeredTabs() { var _this = this; // Each b-tab will register/unregister itself. // We use this to detect when tabs are added/removed // to trigger the update of the tabs. this.$nextTick(function () { requestAF(function () { _this.updateTabs(); }); }); }, tabs: function tabs(newVal, oldVal) { var _this2 = this; // If tabs added, removed, or re-ordered, we emit a `changed` event. // We use `tab._uid` instead of `tab.safeId()`, as the later is changed // in a nextTick if no explicit ID is provided, causing duplicate emits. if (!looseEqual(newVal.map(function (t) { return t._uid; }), oldVal.map(function (t) { return t._uid; }))) { // In a nextTick to ensure currentTab has been set first. this.$nextTick(function () { // We emit shallow copies of the new and old arrays of tabs, to // prevent users from potentially mutating the internal arrays. _this2.$emit('changed', newVal.slice(), oldVal.slice()); }); } }, isMounted: function isMounted(newVal) { var _this3 = this; // Trigger an update after mounted. Needed for tabs inside lazy modals. if (newVal) { requestAF(function () { _this3.updateTabs(); }); } // Enable or disable the observer this.setObserver(newVal); } }, created: function created() { var _this4 = this; // Create private non-reactive props this.$_observer = null; this.currentTab = toInteger(this.value, -1); // For SSR and to make sure only a single tab is shown on mount // We wrap this in a `$nextTick()` to ensure the child tabs have been created this.$nextTick(function () { _this4.updateTabs(); }); }, mounted: function mounted() { var _this5 = this; // Call `updateTabs()` just in case... this.updateTabs(); this.$nextTick(function () { // Flag we are now mounted and to switch to DOM for tab probing. // As this.$slots.default appears to lie about component instances // after b-tabs is destroyed and re-instantiated. // And this.$children does not respect DOM order. _this5.isMounted = true; }); }, /* istanbul ignore next */ deactivated: function deactivated() /* istanbul ignore next */ { this.isMounted = false; }, /* istanbul ignore next */ activated: function activated() /* istanbul ignore next */ { var _this6 = this; this.currentTab = toInteger(this.value, -1); this.$nextTick(function () { _this6.updateTabs(); _this6.isMounted = true; }); }, beforeDestroy: function beforeDestroy() { this.isMounted = false; }, destroyed: function destroyed() { // Ensure no references to child instances exist this.tabs = []; }, methods: { registerTab: function registerTab(tab) { var _this7 = this; if (!arrayIncludes(this.registeredTabs, tab)) { this.registeredTabs.push(tab); tab.$once('hook:destroyed', function () { _this7.unregisterTab(tab); }); } }, unregisterTab: function unregisterTab(tab) { this.registeredTabs = this.registeredTabs.slice().filter(function (t) { return t !== tab; }); }, // DOM observer is needed to detect changes in order of tabs setObserver: function setObserver(on) { this.$_observer && this.$_observer.disconnect(); this.$_observer = null; if (on) { var self = this; /* istanbul ignore next: difficult to test mutation observer in JSDOM */ var handler = function handler() { // We delay the update to ensure that `tab.safeId()` has // updated with the final ID value. self.$nextTick(function () { requestAF(function () { self.updateTabs(); }); }); }; // Watch for changes to <b-tab> sub components this.$_observer = observeDom(this.$refs.tabsContainer, handler, { childList: true, subtree: false, attributes: true, attributeFilter: ['id'] }); } }, getTabs: function getTabs() { // We use `registeredTabs` as the source of truth for child tab components // We also filter out any `<b-tab>` components that are extended // `<b-tab>` with a root child `<b-tab>` // See: https://github.com/bootstrap-vue/bootstrap-vue/issues/3260 var tabs = this.registeredTabs.filter(function (tab) { return tab.$children.filter(function (t) { return t._isTab; }).length === 0; }); // DOM Order of Tabs var order = []; if (this.isMounted && tabs.length > 0) { // We rely on the DOM when mounted to get the 'true' order of the `<b-tab>` children // `querySelectorAll()` always returns elements in document order, regardless of // order specified in the selector var selector = tabs.map(function (tab) { return "#".concat(tab.safeId()); }).join(', '); order = selectAll(selector, this.$el).map(function (el) { return el.id; }).filter(identity); } // Stable sort keeps the original order if not found in the `order` array, // which will be an empty array before mount return stableSort(tabs, function (a, b) { return order.indexOf(a.safeId()) - order.indexOf(b.safeId()); }); }, // Update list of `<b-tab>` children updateTabs: function updateTabs() { // Probe tabs var tabs = this.getTabs(); // Find *last* active non-disabled tab in current tabs // We trust tab state over `currentTab`, in case tabs were added/removed/re-ordered var tabIndex = tabs.indexOf(tabs.slice().reverse().find(function (tab) { return tab.localActive && !tab.disabled; })); // Else try setting to `currentTab` if (tabIndex < 0) { var currentTab = this.currentTab; if (currentTab >= tabs.length) { // Handle last tab being removed, so find the last non-disabled tab tabIndex = tabs.indexOf(tabs.slice().reverse().find(notDisabled)); } else if (tabs[currentTab] && !tabs[currentTab].disabled) { // Current tab is not disabled tabIndex = currentTab; } } // Else find *first* non-disabled tab in current tabs if (tabIndex < 0) { tabIndex = tabs.indexOf(tabs.find(notDisabled)); } // Set the current tab state to active tabs.forEach(function (tab) { // tab.localActive = idx === tabIndex && !tab.disabled tab.localActive = false; }); if (tabs[tabIndex]) { tabs[tabIndex].localActive = true; } // Update the array of tab children this.tabs = tabs; // Set the currentTab index (can be -1 if no non-disabled tabs) this.currentTab = tabIndex; }, // Find a button that controls a tab, given the tab reference // Returns the button vm instance getButtonForTab: function getButtonForTab(tab) { return (this.$refs.buttons || []).find(function (btn) { return btn.tab === tab; }); }, // Force a button to re-render its content, given a <b-tab> instance // Called by <b-tab> on `update()` updateButton: function updateButton(tab) { var button = this.getButtonForTab(tab); if (button && button.$forceUpdate) { button.$forceUpdate(); } }, // Activate a tab given a `<b-tab>` instance // Also accessed by `<b-tab>` activateTab: function activateTab(tab) { var result = false; if (tab) { var index = this.tabs.indexOf(tab); if (!tab.disabled && index > -1 && index !== this.currentTab) { var tabEvt = new BvEvent('activate-tab', { cancelable: true, vueTarget: this, componentId: this.safeId() }); this.$emit(tabEvt.type, index, this.currentTab, tabEvt); if (!tabEvt.defaultPrevented) { result = true; this.currentTab = index; } } } // Couldn't set tab, so ensure v-model is set to `this.currentTab` /* istanbul ignore next: should rarely happen */ if (!result && this.currentTab !== this.value) { this.$emit('input', this.currentTab); } return result; }, // Deactivate a tab given a <b-tab> instance // Accessed by <b-tab> deactivateTab: function deactivateTab(tab) { if (tab) { // Find first non-disabled tab that isn't the one being deactivated // If no tabs are available, then don't deactivate current tab return this.activateTab(this.tabs.filter(function (t) { return t !== tab; }).find(notDisabled)); } /* istanbul ignore next: should never/rarely happen */ return false; }, // Focus a tab button given its <b-tab> instance focusButton: function focusButton(tab) { var _this8 = this; // Wrap in `$nextTick()` to ensure DOM has completed rendering/updating before focusing this.$nextTick(function () { attemptFocus(_this8.getButtonForTab(tab)); }); }, // Emit a click event on a specified <b-tab> component instance emitTabClick: function emitTabClick(tab, evt) { if (isEvent(evt) && tab && tab.$emit && !tab.disabled) { tab.$emit('click', evt); } }, // Click handler clickTab: function clickTab(tab, evt) { this.activateTab(tab); this.emitTabClick(tab, evt); }, // Move to first non-disabled tab firstTab: function firstTab(focus) { var tab = this.tabs.find(notDisabled); if (this.activateTab(tab) && focus) { this.focusButton(tab); this.emitTabClick(tab, focus); } }, // Move to previous non-disabled tab previousTab: function previousTab(focus) { var currentIndex = mathMax(this.currentTab, 0); var tab = this.tabs.slice(0, currentIndex).reverse().find(notDisabled); if (this.activateTab(tab) && focus) { this.focusButton(tab); this.emitTabClick(tab, focus); } }, // Move to next non-disabled tab nextTab: function nextTab(focus) { var currentIndex = mathMax(this.currentTab, -1); var tab = this.tabs.slice(currentIndex + 1).find(notDisabled); if (this.activateTab(tab) && focus) { this.focusButton(tab); this.emitTabClick(tab, focus); } }, // Move to last non-disabled tab lastTab: function lastTab(focus) { var tab = this.tabs.slice().reverse().find(notDisabled); if (this.activateTab(tab) && focus) { this.focusButton(tab); this.emitTabClick(tab, focus); } } }, render: function render(h) { var _this9 = this; var tabs = this.tabs, noKeyNav = this.noKeyNav, firstTab = this.firstTab, previousTab = this.previousTab, nextTab = this.nextTab, lastTab = this.lastTab; // Currently active tab var activeTab = tabs.find(function (tab) { return tab.localActive && !tab.disabled; }); // Tab button to allow focusing when no active tab found (keynav only) var fallbackTab = tabs.find(function (tab) { return !tab.disabled; }); // For each `<b-tab>` found create the tab buttons var buttons = tabs.map(function (tab, index) { var tabIndex = null; // Ensure at least one tab button is focusable when keynav enabled (if possible) if (!noKeyNav) { // Buttons are not in tab index unless active, or a fallback tab tabIndex = -1; if (activeTab === tab || !activeTab && fallbackTab === tab) { // Place tab button in tab sequence tabIndex = null; } } return h(BVTabButton, { key: tab._uid || index, ref: 'buttons', // Needed to make `this.$refs.buttons` an array refInFor: true, props: { tab: tab, tabs: tabs, id: tab.controlledBy || (tab.safeId ? tab.safeId("_BV_tab_button_") : null), controls: tab.safeId ? tab.safeId() : null, tabIndex: tabIndex, setSize: tabs.length, posInSet: index + 1, noKeyNav: noKeyNav }, on: { click: function click(evt) { _this9.clickTab(tab, evt); }, first: firstTab, prev: previousTab, next: nextTab, last: lastTab } }); }); // Nav var nav = h(BNav, { ref: 'nav', class: this.localNavClass, attrs: { role: 'tablist', id: this.safeId('_BV_tab_controls_') }, props: { fill: this.fill, justified: this.justified, align: this.align, tabs: !this.noNavStyle && !this.pills, pills: !this.noNavStyle && this.pills, vertical: this.vertical, small: this.small, cardHeader: this.card && !this.vertical } }, [this.normalizeSlot('tabs-start') || h(), buttons, this.normalizeSlot('tabs-end') || h()]); nav = h('div', { key: 'bv-tabs-nav', class: [{ 'card-header': this.card && !this.vertical && !this.end, 'card-footer': this.card && !this.vertical && this.end, 'col-auto': this.vertical }, this.navWrapperClass] }, [nav]); var empty = h(); if (!tabs || tabs.length === 0) { empty = h('div', { key: 'bv-empty-tab', class: ['tab-pane', 'active', { 'card-body': this.card }] }, this.normalizeSlot('empty')); } // Main content section var content = h('div', { ref: 'tabsContainer', key: 'bv-tabs-container', staticClass: 'tab-content', class: [{ col: this.vertical }, this.contentClass], attrs: { id: this.safeId('_BV_tab_container_') } }, concat(this.normalizeSlot(), empty)); // Render final output return h(this.tag, { staticClass: 'tabs', class: { row: this.vertical, 'no-gutters': this.vertical && this.card }, attrs: { id: this.safeId() } }, [this.end ? content : h(), [nav], this.end ? h() : content]); } });