bootstrap-vue
Version:
BootstrapVue, with over 40 plugins and more than 80 custom components, custom directives, and over 300 icons, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-AR
144 lines (129 loc) • 5.06 kB
JavaScript
import looseEqual from '../../utils/loose-equal'
import { addClass, hasAttr, removeAttr, removeClass, setAttr } from '../../utils/dom'
import { isBrowser } from '../../utils/env'
import { bindTargets, getTargets, unbindTargets } from '../../utils/target'
// Target listen types
const listenTypes = { click: true }
// Property key for handler storage
const BV_TOGGLE = '__BV_toggle__'
const BV_TOGGLE_STATE = '__BV_toggle_STATE__'
const BV_TOGGLE_CONTROLS = '__BV_toggle_CONTROLS__'
const BV_TOGGLE_TARGETS = '__BV_toggle_TARGETS__'
// Emitted control event for collapse (emitted to collapse)
const EVENT_TOGGLE = 'bv::toggle::collapse'
// Listen to event for toggle state update (emitted by collapse)
const EVENT_STATE = 'bv::collapse::state'
// Private event emitted on $root to ensure the toggle state is always synced.
// Gets emitted even if the state of b-collapse has not changed.
// This event is NOT to be documented as people should not be using it.
const EVENT_STATE_SYNC = 'bv::collapse::sync::state'
// Private event we send to collapse to request state update sync event
const EVENT_STATE_REQUEST = 'bv::request::collapse::state'
// Reset and remove a property from the provided element
const resetProp = (el, prop) => {
el[prop] = null
delete el[prop]
}
// Handle targets update
const handleTargets = ({ targets, vnode }) => {
targets.forEach(target => {
vnode.context.$root.$emit(EVENT_TOGGLE, target)
})
}
// Handle directive updates
/* istanbul ignore next: not easy to test */
const handleUpdate = (el, binding, vnode) => {
if (!isBrowser) {
return
}
if (!looseEqual(getTargets(binding), el[BV_TOGGLE_TARGETS])) {
// Targets have changed, so update accordingly
unbindTargets(vnode, binding, listenTypes)
const targets = bindTargets(vnode, binding, listenTypes, handleTargets)
// Update targets array to element
el[BV_TOGGLE_TARGETS] = targets
// Add aria attributes to element
el[BV_TOGGLE_CONTROLS] = targets.join(' ')
// ensure aria-controls is up to date
setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
// Request a state update from targets so that we can ensure
// expanded state is correct
targets.forEach(target => {
vnode.context.$root.$emit(EVENT_STATE_REQUEST, target)
})
}
// Ensure the collapse class and aria-* attributes persist
// after element is updated (either by parent re-rendering
// or changes to this element or its contents
if (el[BV_TOGGLE_STATE] === true) {
addClass(el, 'collapsed')
setAttr(el, 'aria-expanded', 'true')
} else if (el[BV_TOGGLE_STATE] === false) {
removeClass(el, 'collapsed')
setAttr(el, 'aria-expanded', 'false')
}
setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
}
/*
* Export our directive
*/
export const VBToggle = {
bind(el, binding, vnode) {
const targets = bindTargets(vnode, binding, listenTypes, handleTargets)
if (isBrowser && vnode.context && targets.length > 0) {
// Add targets array to element
el[BV_TOGGLE_TARGETS] = targets
// Add aria attributes to element
el[BV_TOGGLE_CONTROLS] = targets.join(' ')
// State is initially collapsed until we receive a state event
el[BV_TOGGLE_STATE] = false
setAttr(el, 'aria-controls', el[BV_TOGGLE_CONTROLS])
setAttr(el, 'aria-expanded', 'false')
// If element is not a button, we add `role="button"` for accessibility
if (el.tagName !== 'BUTTON' && !hasAttr(el, 'role')) {
setAttr(el, 'role', 'button')
}
// Toggle state handler
const toggleDirectiveHandler = (id, state) => {
const targets = el[BV_TOGGLE_TARGETS] || []
if (targets.indexOf(id) !== -1) {
// Set aria-expanded state
setAttr(el, 'aria-expanded', state ? 'true' : 'false')
// Set/Clear 'collapsed' class state
el[BV_TOGGLE_STATE] = state
if (state) {
removeClass(el, 'collapsed')
} else {
addClass(el, 'collapsed')
}
}
}
// Store the toggle handler on the element
el[BV_TOGGLE] = toggleDirectiveHandler
// Listen for toggle state changes (public)
vnode.context.$root.$on(EVENT_STATE, el[BV_TOGGLE])
// Listen for toggle state sync (private)
vnode.context.$root.$on(EVENT_STATE_SYNC, el[BV_TOGGLE])
}
},
componentUpdated: handleUpdate,
updated: handleUpdate,
unbind(el, binding, vnode) /* istanbul ignore next */ {
unbindTargets(vnode, binding, listenTypes)
// Remove our $root listener
if (el[BV_TOGGLE]) {
vnode.context.$root.$off(EVENT_STATE, el[BV_TOGGLE])
vnode.context.$root.$off(EVENT_STATE_SYNC, el[BV_TOGGLE])
}
// Reset custom props
resetProp(el, BV_TOGGLE)
resetProp(el, BV_TOGGLE_STATE)
resetProp(el, BV_TOGGLE_CONTROLS)
resetProp(el, BV_TOGGLE_TARGETS)
// Reset classes/attrs
removeClass(el, 'collapsed')
removeAttr(el, 'aria-expanded')
removeAttr(el, 'aria-controls')
removeAttr(el, 'role')
}
}