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

257 lines (253 loc) 7.19 kB
import Vue from '../../vue' import { NAME_COLLAPSE } from '../../constants/components' import { EVENT_OPTIONS_NO_CAPTURE } from '../../constants/events' import { SLOT_NAME_DEFAULT } from '../../constants/slot-names' import { BVCollapse } from '../../utils/bv-collapse' import { addClass, hasClass, removeClass, closest, matches, getCS } from '../../utils/dom' import { isBrowser } from '../../utils/env' import { eventOnOff } from '../../utils/events' import idMixin from '../../mixins/id' import listenOnRootMixin from '../../mixins/listen-on-root' import normalizeSlotMixin from '../../mixins/normalize-slot' import { EVENT_TOGGLE, EVENT_STATE, EVENT_STATE_REQUEST, EVENT_STATE_SYNC } from '../../directives/toggle/toggle' // --- Constants --- // Accordion event name we emit on `$root` const EVENT_ACCORDION = 'bv::collapse::accordion' // --- Main component --- // @vue/component export const BCollapse = /*#__PURE__*/ Vue.extend({ name: NAME_COLLAPSE, mixins: [idMixin, listenOnRootMixin, normalizeSlotMixin], model: { prop: 'visible', event: 'input' }, props: { isNav: { type: Boolean, default: false }, accordion: { type: String // default: null }, visible: { type: Boolean, default: false }, tag: { type: String, default: 'div' }, appear: { // If `true` (and `visible` is `true` on mount), animate initially visible type: Boolean, default: false } }, data() { return { show: this.visible, transitioning: false } }, computed: { classObject() { return { 'navbar-collapse': this.isNav, collapse: !this.transitioning, show: this.show && !this.transitioning } } }, watch: { visible(newVal) { if (newVal !== this.show) { this.show = newVal } }, show(newVal, oldVal) { if (newVal !== oldVal) { this.emitState() } } }, created() { this.show = this.visible }, mounted() { this.show = this.visible // Listen for toggle events to open/close us this.listenOnRoot(EVENT_TOGGLE, this.handleToggleEvt) // Listen to other collapses for accordion events this.listenOnRoot(EVENT_ACCORDION, this.handleAccordionEvt) if (this.isNav) { // Set up handlers this.setWindowEvents(true) this.handleResize() } this.$nextTick(() => { this.emitState() }) // Listen for "Sync state" requests from `v-b-toggle` this.listenOnRoot(EVENT_STATE_REQUEST, id => { if (id === this.safeId()) { this.$nextTick(this.emitSync) } }) }, updated() { // Emit a private event every time this component updates to ensure // the toggle button is in sync with the collapse's state // It is emitted regardless if the visible state changes this.emitSync() }, /* istanbul ignore next */ deactivated() /* istanbul ignore next */ { if (this.isNav) { this.setWindowEvents(false) } }, /* istanbul ignore next */ activated() /* istanbul ignore next */ { if (this.isNav) { this.setWindowEvents(true) } this.emitSync() }, beforeDestroy() { // Trigger state emit if needed this.show = false if (this.isNav && isBrowser) { this.setWindowEvents(false) } }, methods: { setWindowEvents(on) { eventOnOff(on, window, 'resize', this.handleResize, EVENT_OPTIONS_NO_CAPTURE) eventOnOff(on, window, 'orientationchange', this.handleResize, EVENT_OPTIONS_NO_CAPTURE) }, toggle() { this.show = !this.show }, onEnter() { this.transitioning = true // This should be moved out so we can add cancellable events this.$emit('show') }, onAfterEnter() { this.transitioning = false this.$emit('shown') }, onLeave() { this.transitioning = true // This should be moved out so we can add cancellable events this.$emit('hide') }, onAfterLeave() { this.transitioning = false this.$emit('hidden') }, emitState() { this.$emit('input', this.show) // Let `v-b-toggle` know the state of this collapse this.emitOnRoot(EVENT_STATE, this.safeId(), this.show) if (this.accordion && this.show) { // Tell the other collapses in this accordion to close this.emitOnRoot(EVENT_ACCORDION, this.safeId(), this.accordion) } }, emitSync() { // Emit a private event every time this component updates to ensure // the toggle button is in sync with the collapse's state // It is emitted regardless if the visible state changes this.emitOnRoot(EVENT_STATE_SYNC, this.safeId(), this.show) }, checkDisplayBlock() { // Check to see if the collapse has `display: block !important` set // We can't set `display: none` directly on `this.$el`, as it would // trigger a new transition to start (or cancel a current one) const restore = hasClass(this.$el, 'show') removeClass(this.$el, 'show') const isBlock = getCS(this.$el).display === 'block' if (restore) { addClass(this.$el, 'show') } return isBlock }, clickHandler(evt) { // If we are in a nav/navbar, close the collapse when non-disabled link clicked const el = evt.target if (!this.isNav || !el || getCS(this.$el).display !== 'block') { /* istanbul ignore next: can't test getComputedStyle in JSDOM */ return } if (matches(el, '.nav-link,.dropdown-item') || closest('.nav-link,.dropdown-item', el)) { if (!this.checkDisplayBlock()) { // Only close the collapse if it is not forced to be `display: block !important` this.show = false } } }, handleToggleEvt(target) { if (target !== this.safeId()) { return } this.toggle() }, handleAccordionEvt(openedId, accordion) { if (!this.accordion || accordion !== this.accordion) { return } if (openedId === this.safeId()) { // Open this collapse if not shown if (!this.show) { this.toggle() } } else { // Close this collapse if shown if (this.show) { this.toggle() } } }, handleResize() { // Handler for orientation/resize to set collapsed state in nav/navbar this.show = getCS(this.$el).display === 'block' } }, render(h) { const scope = { visible: this.show, close: () => (this.show = false) } const content = h( this.tag, { class: this.classObject, directives: [{ name: 'show', value: this.show }], attrs: { id: this.safeId() }, on: { click: this.clickHandler } }, [this.normalizeSlot(SLOT_NAME_DEFAULT, scope)] ) return h( BVCollapse, { props: { appear: this.appear }, on: { enter: this.onEnter, afterEnter: this.onAfterEnter, leave: this.onLeave, afterLeave: this.onAfterLeave } }, [content] ) } })