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
273 lines (259 loc) • 8.23 kB
JavaScript
import { extend } from '../../vue'
import { NAME_COLLAPSE } from '../../constants/components'
import { CLASS_NAME_SHOW } from '../../constants/classes'
import { IS_BROWSER } from '../../constants/env'
import {
EVENT_NAME_HIDDEN,
EVENT_NAME_HIDE,
EVENT_NAME_SHOW,
EVENT_NAME_SHOWN,
EVENT_OPTIONS_NO_CAPTURE
} from '../../constants/events'
import { PROP_TYPE_BOOLEAN, PROP_TYPE_STRING } from '../../constants/props'
import { SLOT_NAME_DEFAULT } from '../../constants/slots'
import { addClass, hasClass, removeClass, closest, matches, getCS } from '../../utils/dom'
import { getRootActionEventName, getRootEventName, eventOnOff } from '../../utils/events'
import { makeModelMixin } from '../../utils/model'
import { sortKeys } from '../../utils/object'
import { makeProp, makePropsConfigurable } from '../../utils/props'
import { idMixin, props as idProps } from '../../mixins/id'
import { listenOnRootMixin } from '../../mixins/listen-on-root'
import { normalizeSlotMixin } from '../../mixins/normalize-slot'
import { BVCollapse } from './helpers/bv-collapse'
// --- Constants ---
const ROOT_ACTION_EVENT_NAME_TOGGLE = getRootActionEventName(NAME_COLLAPSE, 'toggle')
const ROOT_ACTION_EVENT_NAME_REQUEST_STATE = getRootActionEventName(NAME_COLLAPSE, 'request-state')
const ROOT_EVENT_NAME_ACCORDION = getRootEventName(NAME_COLLAPSE, 'accordion')
const ROOT_EVENT_NAME_STATE = getRootEventName(NAME_COLLAPSE, 'state')
const ROOT_EVENT_NAME_SYNC_STATE = getRootEventName(NAME_COLLAPSE, 'sync-state')
const {
mixin: modelMixin,
props: modelProps,
prop: MODEL_PROP_NAME,
event: MODEL_EVENT_NAME
} = makeModelMixin('visible', { type: PROP_TYPE_BOOLEAN, defaultValue: false })
// --- Props ---
export const props = makePropsConfigurable(
sortKeys({
...idProps,
...modelProps,
// If `true` (and `visible` is `true` on mount), animate initially visible
accordion: makeProp(PROP_TYPE_STRING),
appear: makeProp(PROP_TYPE_BOOLEAN, false),
isNav: makeProp(PROP_TYPE_BOOLEAN, false),
tag: makeProp(PROP_TYPE_STRING, 'div')
}),
NAME_COLLAPSE
)
// --- Main component ---
// @vue/component
export const BCollapse = /*#__PURE__*/ extend({
name: NAME_COLLAPSE,
mixins: [idMixin, modelMixin, normalizeSlotMixin, listenOnRootMixin],
props,
data() {
return {
show: this[MODEL_PROP_NAME],
transitioning: false
}
},
computed: {
classObject() {
const { transitioning } = this
return {
'navbar-collapse': this.isNav,
collapse: !transitioning,
show: this.show && !transitioning
}
},
slotScope() {
return {
visible: this.show,
close: () => {
this.show = false
}
}
}
},
watch: {
[MODEL_PROP_NAME](newValue) {
if (newValue !== this.show) {
this.show = newValue
}
},
show(newValue, oldValue) {
if (newValue !== oldValue) {
this.emitState()
}
}
},
created() {
this.show = this[MODEL_PROP_NAME]
},
mounted() {
this.show = this[MODEL_PROP_NAME]
// Listen for toggle events to open/close us
this.listenOnRoot(ROOT_ACTION_EVENT_NAME_TOGGLE, this.handleToggleEvent)
// Listen to other collapses for accordion events
this.listenOnRoot(ROOT_EVENT_NAME_ACCORDION, this.handleAccordionEvent)
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(ROOT_ACTION_EVENT_NAME_REQUEST_STATE, 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() {
if (this.isNav) {
this.setWindowEvents(false)
}
},
/* istanbul ignore next */
activated() {
if (this.isNav) {
this.setWindowEvents(true)
}
this.emitSync()
},
beforeDestroy() {
// Trigger state emit if needed
this.show = false
if (this.isNav && IS_BROWSER) {
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(EVENT_NAME_SHOW)
},
onAfterEnter() {
this.transitioning = false
this.$emit(EVENT_NAME_SHOWN)
},
onLeave() {
this.transitioning = true
// This should be moved out so we can add cancellable events
this.$emit(EVENT_NAME_HIDE)
},
onAfterLeave() {
this.transitioning = false
this.$emit(EVENT_NAME_HIDDEN)
},
emitState() {
const { show, accordion } = this
const id = this.safeId()
this.$emit(MODEL_EVENT_NAME, show)
// Let `v-b-toggle` know the state of this collapse
this.emitOnRoot(ROOT_EVENT_NAME_STATE, id, show)
if (accordion && show) {
// Tell the other collapses in this accordion to close
this.emitOnRoot(ROOT_EVENT_NAME_ACCORDION, id, 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(ROOT_EVENT_NAME_SYNC_STATE, 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 { $el } = this
const restore = hasClass($el, CLASS_NAME_SHOW)
removeClass($el, CLASS_NAME_SHOW)
const isBlock = getCS($el).display === 'block'
if (restore) {
addClass($el, CLASS_NAME_SHOW)
}
return isBlock
},
clickHandler(event) {
const { target: el } = event
// If we are in a nav/navbar, close the collapse when non-disabled link clicked
/* istanbul ignore next: can't test `getComputedStyle()` in JSDOM */
if (!this.isNav || !el || getCS(this.$el).display !== 'block') {
return
}
// Only close the collapse if it is not forced to be `display: block !important`
if (
(matches(el, '.nav-link,.dropdown-item') || closest('.nav-link,.dropdown-item', el)) &&
!this.checkDisplayBlock()
) {
this.show = false
}
},
handleToggleEvent(id) {
if (id === this.safeId()) {
this.toggle()
}
},
handleAccordionEvent(openedId, openAccordion) {
const { accordion, show } = this
if (!accordion || accordion !== openAccordion) {
return
}
const isThis = openedId === this.safeId()
// Open this collapse if not shown or
// close this collapse if shown
if ((isThis && !show) || (!isThis && 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 { appear } = this
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, this.slotScope)
)
return h(
BVCollapse,
{
props: { appear },
on: {
enter: this.onEnter,
afterEnter: this.onAfterEnter,
leave: this.onLeave,
afterLeave: this.onAfterLeave
}
},
[$content]
)
}
})