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

275 lines (242 loc) 9.18 kB
import { NAME_COLLAPSE } from '../../constants/components' import { IS_BROWSER } from '../../constants/env' import { EVENT_OPTIONS_PASSIVE } from '../../constants/events' import { CODE_ENTER, CODE_SPACE } from '../../constants/key-codes' import { RX_HASH, RX_HASH_ID, RX_SPACE_SPLIT } from '../../constants/regex' import { arrayIncludes, concat } from '../../utils/array' import { getInstanceFromDirective } from '../../utils/get-instance-from-directive' import { addClass, getAttr, hasAttr, isDisabled, isTag, removeAttr, removeClass, removeStyle, requestAF, setAttr, setStyle } from '../../utils/dom' import { getRootActionEventName, getRootEventName, eventOn, eventOff } from '../../utils/events' import { isString } from '../../utils/inspect' import { looseEqual } from '../../utils/loose-equal' import { keys } from '../../utils/object' import { getEventRoot } from '../../utils/get-event-root' // --- Constants --- // Classes to apply to trigger element const CLASS_BV_TOGGLE_COLLAPSED = 'collapsed' const CLASS_BV_TOGGLE_NOT_COLLAPSED = 'not-collapsed' // Property key for handler storage const BV_BASE = '__BV_toggle' // Root event listener property (Function) const BV_TOGGLE_ROOT_HANDLER = `${BV_BASE}_HANDLER__` // Trigger element click handler property (Function) const BV_TOGGLE_CLICK_HANDLER = `${BV_BASE}_CLICK__` // Target visibility state property (Boolean) const BV_TOGGLE_STATE = `${BV_BASE}_STATE__` // Target ID list property (Array) const BV_TOGGLE_TARGETS = `${BV_BASE}_TARGETS__` // Commonly used strings const STRING_FALSE = 'false' const STRING_TRUE = 'true' // Commonly used attribute names const ATTR_ARIA_CONTROLS = 'aria-controls' const ATTR_ARIA_EXPANDED = 'aria-expanded' const ATTR_ROLE = 'role' const ATTR_TABINDEX = 'tabindex' // Commonly used style properties const STYLE_OVERFLOW_ANCHOR = 'overflow-anchor' // Emitted control event for collapse (emitted to collapse) const ROOT_ACTION_EVENT_NAME_TOGGLE = getRootActionEventName(NAME_COLLAPSE, 'toggle') // Listen to event for toggle state update (emitted by collapse) const ROOT_EVENT_NAME_STATE = getRootEventName(NAME_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 ROOT_EVENT_NAME_SYNC_STATE = getRootEventName(NAME_COLLAPSE, 'sync-state') // Private event we send to collapse to request state update sync event const ROOT_ACTION_EVENT_NAME_REQUEST_STATE = getRootActionEventName(NAME_COLLAPSE, 'request-state') const KEYDOWN_KEY_CODES = [CODE_ENTER, CODE_SPACE] // --- Helper methods --- const isNonStandardTag = el => !arrayIncludes(['button', 'a'], el.tagName.toLowerCase()) const getTargets = ({ modifiers, arg, value }, el) => { // Any modifiers are considered target IDs const targets = keys(modifiers || {}) // If value is a string, split out individual targets (if space delimited) value = isString(value) ? value.split(RX_SPACE_SPLIT) : value // Support target ID as link href (`href="#id"`) if (isTag(el.tagName, 'a')) { const href = getAttr(el, 'href') || '' if (RX_HASH_ID.test(href)) { targets.push(href.replace(RX_HASH, '')) } } // Add ID from `arg` (if provided), and support value // as a single string ID or an array of string IDs // If `value` is not an array or string, then it gets filtered out concat(arg, value).forEach(t => isString(t) && targets.push(t)) // Return only unique and truthy target IDs return targets.filter((t, index, arr) => t && arr.indexOf(t) === index) } const removeClickListener = el => { const handler = el[BV_TOGGLE_CLICK_HANDLER] if (handler) { eventOff(el, 'click', handler, EVENT_OPTIONS_PASSIVE) eventOff(el, 'keydown', handler, EVENT_OPTIONS_PASSIVE) } el[BV_TOGGLE_CLICK_HANDLER] = null } const addClickListener = (el, instance) => { removeClickListener(el) if (instance) { const handler = event => { if ( !(event.type === 'keydown' && !arrayIncludes(KEYDOWN_KEY_CODES, event.keyCode)) && !isDisabled(el) ) { const targets = el[BV_TOGGLE_TARGETS] || [] targets.forEach(target => { getEventRoot(instance).$emit(ROOT_ACTION_EVENT_NAME_TOGGLE, target) }) } } el[BV_TOGGLE_CLICK_HANDLER] = handler eventOn(el, 'click', handler, EVENT_OPTIONS_PASSIVE) if (isNonStandardTag(el)) { eventOn(el, 'keydown', handler, EVENT_OPTIONS_PASSIVE) } } } const removeRootListeners = (el, instance) => { if (el[BV_TOGGLE_ROOT_HANDLER] && instance) { getEventRoot(instance).$off( [ROOT_EVENT_NAME_STATE, ROOT_EVENT_NAME_SYNC_STATE], el[BV_TOGGLE_ROOT_HANDLER] ) } el[BV_TOGGLE_ROOT_HANDLER] = null } const addRootListeners = (el, instance) => { removeRootListeners(el, instance) if (instance) { const handler = (id, state) => { // `state` will be `true` if target is expanded if (arrayIncludes(el[BV_TOGGLE_TARGETS] || [], id)) { // Set/Clear 'collapsed' visibility class state el[BV_TOGGLE_STATE] = state // Set `aria-expanded` and class state on trigger element setToggleState(el, state) } } el[BV_TOGGLE_ROOT_HANDLER] = handler // Listen for toggle state changes (public) and sync (private) getEventRoot(instance).$on([ROOT_EVENT_NAME_STATE, ROOT_EVENT_NAME_SYNC_STATE], handler) } } const setToggleState = (el, state) => { // State refers to the visibility of the collapse/sidebar if (state) { removeClass(el, CLASS_BV_TOGGLE_COLLAPSED) addClass(el, CLASS_BV_TOGGLE_NOT_COLLAPSED) setAttr(el, ATTR_ARIA_EXPANDED, STRING_TRUE) } else { removeClass(el, CLASS_BV_TOGGLE_NOT_COLLAPSED) addClass(el, CLASS_BV_TOGGLE_COLLAPSED) setAttr(el, ATTR_ARIA_EXPANDED, STRING_FALSE) } } // Reset and remove a property from the provided element const resetProp = (el, prop) => { el[prop] = null delete el[prop] } // Handle directive updates const handleUpdate = (el, binding, vnode) => { /* istanbul ignore next: should never happen */ if (!IS_BROWSER || !getInstanceFromDirective(vnode, binding)) { return } // If element is not a button or link, we add `role="button"` // and `tabindex="0"` for accessibility reasons if (isNonStandardTag(el)) { if (!hasAttr(el, ATTR_ROLE)) { setAttr(el, ATTR_ROLE, 'button') } if (!hasAttr(el, ATTR_TABINDEX)) { setAttr(el, ATTR_TABINDEX, '0') } } // 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) setToggleState(el, el[BV_TOGGLE_STATE]) // Parse list of target IDs const targets = getTargets(binding, el) // Ensure the `aria-controls` hasn't been overwritten // or removed when vnode updates // Also ensure to set `overflow-anchor` to `none` to prevent // the browser's scroll anchoring behavior /* istanbul ignore else */ if (targets.length > 0) { setAttr(el, ATTR_ARIA_CONTROLS, targets.join(' ')) setStyle(el, STYLE_OVERFLOW_ANCHOR, 'none') } else { removeAttr(el, ATTR_ARIA_CONTROLS) removeStyle(el, STYLE_OVERFLOW_ANCHOR) } // Add/Update our click listener(s) // Wrap in a `requestAF()` to allow any previous // click handling to occur first requestAF(() => { addClickListener(el, getInstanceFromDirective(vnode, binding)) }) // If targets array has changed, update if (!looseEqual(targets, el[BV_TOGGLE_TARGETS])) { // Update targets array to element storage el[BV_TOGGLE_TARGETS] = targets // Ensure `aria-controls` is up to date // Request a state update from targets so that we can // ensure expanded state is correct (in most cases) targets.forEach(target => { getEventRoot(getInstanceFromDirective(vnode, binding)).$emit( ROOT_ACTION_EVENT_NAME_REQUEST_STATE, target ) }) } } /* * Export our directive */ export const VBToggle = { bind(el, binding, vnode) { // State is initially collapsed until we receive a state event el[BV_TOGGLE_STATE] = false // Assume no targets initially el[BV_TOGGLE_TARGETS] = [] // Add our root listeners addRootListeners(el, getInstanceFromDirective(vnode, binding)) // Initial update of trigger handleUpdate(el, binding, vnode) }, componentUpdated: handleUpdate, updated: handleUpdate, unbind(el, binding, vnode) { removeClickListener(el) // Remove our $root listener removeRootListeners(el, getInstanceFromDirective(vnode, binding)) // Reset custom props resetProp(el, BV_TOGGLE_ROOT_HANDLER) resetProp(el, BV_TOGGLE_CLICK_HANDLER) resetProp(el, BV_TOGGLE_STATE) resetProp(el, BV_TOGGLE_TARGETS) // Reset classes/attrs/styles removeClass(el, CLASS_BV_TOGGLE_COLLAPSED) removeClass(el, CLASS_BV_TOGGLE_NOT_COLLAPSED) removeAttr(el, ATTR_ARIA_EXPANDED) removeAttr(el, ATTR_ARIA_CONTROLS) removeAttr(el, ATTR_ROLE) removeStyle(el, STYLE_OVERFLOW_ANCHOR) } }