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
915 lines (900 loc) • 31.5 kB
JavaScript
// Tooltip "Class" (Built as a renderless Vue instance)
//
// Handles trigger events, etc.
// Instantiates template on demand
import Vue from '../../../utils/vue'
import getScopId from '../../../utils/get-scope-id'
import looseEqual from '../../../utils/loose-equal'
import noop from '../../../utils/noop'
import { arrayIncludes, concat, from as arrayFrom } from '../../../utils/array'
import {
isElement,
isDisabled,
isVisible,
closest,
contains,
select,
getById,
hasClass,
getAttr,
hasAttr,
setAttr,
removeAttr,
eventOn,
eventOff
} from '../../../utils/dom'
import {
isFunction,
isNumber,
isPlainObject,
isString,
isUndefined,
isUndefinedOrNull
} from '../../../utils/inspect'
import { keys } from '../../../utils/object'
import { warn } from '../../../utils/warn'
import { BvEvent } from '../../../utils/bv-event.class'
import { BVTooltipTemplate } from './bv-tooltip-template'
const NAME = 'BVTooltip'
// Modal container selector for appending tooltip/popover
const MODAL_SELECTOR = '.modal-content'
// Modal `$root` hidden event
const MODAL_CLOSE_EVENT = 'bv::modal::hidden'
// For dropdown sniffing
const DROPDOWN_CLASS = 'dropdown'
const DROPDOWN_OPEN_SELECTOR = '.dropdown-menu.show'
// Options for Native Event Listeners (since we never call preventDefault)
const EvtOpts = { passive: true, capture: false }
// Data specific to popper and template
// We don't use props, as we need reactivity (we can't pass reactive props)
const templateData = {
// Text string or Scoped slot function
title: '',
// Text string or Scoped slot function
content: '',
// String
variant: null,
// String, Array, Object
customClass: null,
// String or array of Strings (overwritten by BVPopper)
triggers: '',
// String (overwritten by BVPopper)
placement: 'auto',
// String or array of strings
fallbackPlacement: 'flip',
// Element or Component reference (or function that returns element) of
// the element that will have the trigger events bound, and is also
// default element for positioning
target: null,
// HTML ID, Element or Component reference
container: null, // 'body'
// Boolean
noFade: false,
// 'scrollParent', 'viewport', 'window', Element, or Component reference
boundary: 'scrollParent',
// Tooltip/popover will try and stay away from
// boundary edge by this many pixels (Number)
boundaryPadding: 5,
// Arrow offset (Number)
offset: 0,
// Hover/focus delay (Number or Object)
delay: 0,
// Arrow of Tooltip/popover will try and stay away from
// the edge of tooltip/popover edge by this many pixels
arrowPadding: 6,
// Interactive state (Boolean)
interactive: true,
// Disabled state (Boolean)
disabled: false,
// ID to use for tooltip/popover
id: null,
// Flag used by directives only, for HTML content
html: false
}
// @vue/component
export const BVTooltip = /*#__PURE__*/ Vue.extend({
name: NAME,
props: {
// None
},
data() {
return {
// BTooltip/BPopover/VBTooltip/VBPopover will update this data
// Via the exposed updateData() method on this instance
// BVPopover will override some of these defaults
...templateData,
// State management data
activeTrigger: {
// manual: false,
hover: false,
click: false,
focus: false
},
localShow: false
}
},
computed: {
templateType() {
// Overwritten by BVPopover
return 'tooltip'
},
computedId() {
return this.id || `__bv_${this.templateType}_${this._uid}__`
},
computedDelay() {
// Normalizes delay into object form
const delay = { show: 0, hide: 0 }
if (isPlainObject(this.delay)) {
delay.show = Math.max(parseInt(this.delay.show, 10) || 0, 0)
delay.hide = Math.max(parseInt(this.delay.hide, 10) || 0, 0)
} else if (isNumber(this.delay) || isString(this.delay)) {
delay.show = delay.hide = Math.max(parseInt(this.delay, 10) || 0, 0)
}
return delay
},
computedTriggers() {
// Returns the triggers in sorted array form
// TODO: Switch this to object form for easier lookup
return concat(this.triggers)
.filter(Boolean)
.join(' ')
.trim()
.toLowerCase()
.split(/\s+/)
.sort()
},
isWithActiveTrigger() {
for (const trigger in this.activeTrigger) {
if (this.activeTrigger[trigger]) {
return true
}
}
return false
},
computedTemplateData() {
return {
title: this.title,
content: this.content,
variant: this.variant,
customClass: this.customClass,
noFade: this.noFade,
interactive: this.interactive
}
}
},
watch: {
computedTriggers(newTriggers, oldTriggers) {
// Triggers have changed, so re-register them
/* istanbul ignore next */
if (!looseEqual(newTriggers, oldTriggers)) {
this.$nextTick(() => {
// Disable trigger listeners
this.unListen()
// Clear any active triggers that are no longer in the list of triggers
oldTriggers.forEach(trigger => {
if (!arrayIncludes(newTriggers, trigger)) {
if (this.activeTrigger[trigger]) {
this.activeTrigger[trigger] = false
}
}
})
// Re-enable the trigger listeners
this.listen()
})
}
},
computedTemplateData() {
// If any of the while open reactive "props" change,
// ensure that the template updates accordingly
this.handleTemplateUpdate()
},
disabled(newVal) {
newVal ? this.disable() : this.enable()
}
},
created() {
// Create non-reactive properties
this.$_tip = null
this.$_hoverTimeout = null
this.$_hoverState = ''
this.$_visibleInterval = null
this.$_enabled = !this.disabled
this.$_noop = noop.bind(this)
// Destroy ourselves when the parent is destroyed
if (this.$parent) {
this.$parent.$once('hook:beforeDestroy', this.$destroy)
}
this.$nextTick(() => {
const target = this.getTarget()
if (target && contains(document.body, target)) {
// Copy the parent's scoped style attribute
this.scopeId = getScopId(this.$parent)
// Set up all trigger handlers and listeners
this.listen()
} else {
/* istanbul ignore next */
warn('Unable to find target element in document.', this.templateType)
}
})
},
updated() /* istanbul ignore next */ {
// Usually called when the slots/data changes
this.$nextTick(this.handleTemplateUpdate)
},
deactivated() /* istanbul ignore next */ {
// In a keepalive that has been deactivated, so hide
// the tooltip/popover if it is showing
this.forceHide()
},
beforeDestroy() /* istanbul ignore next */ {
// Remove all handler/listeners
this.unListen()
this.setWhileOpenListeners(false)
// Clear any timeouts/intervals
this.clearHoverTimeout()
this.clearVisibilityInterval()
// Destroy the template
this.destroyTemplate()
},
methods: {
// --- Methods for creating and destroying the template ---
getTemplate() {
// Overridden by BVPopover
return BVTooltipTemplate
},
updateData(data = {}) {
// Method for updating popper/template data
// We only update data if it exists, and has not changed
let titleUpdated = false
keys(templateData).forEach(prop => {
if (!isUndefined(data[prop]) && this[prop] !== data[prop]) {
this[prop] = data[prop]
if (prop === 'title') {
titleUpdated = true
}
}
})
if (titleUpdated && this.localShow) {
// If the title has updated, we may need to handle the title
// attribute on the trigger target. We only do this while the
// template is open
this.fixTitle()
}
},
createTemplateAndShow() {
// Creates the template instance and show it
const container = this.getContainer()
const Template = this.getTemplate()
const $tip = (this.$_tip = new Template({
parent: this,
// The following is not reactive to changes in the props data
propsData: {
// These values cannot be changed while template is showing
id: this.computedId,
html: this.html,
placement: this.placement,
fallbackPlacement: this.fallbackPlacement,
target: this.getPlacementTarget(),
boundary: this.getBoundary(),
// Ensure the following are integers
offset: parseInt(this.offset, 10) || 0,
arrowPadding: parseInt(this.arrowPadding, 10) || 0,
boundaryPadding: parseInt(this.boundaryPadding, 10) || 0
}
}))
// We set the initial reactive data (values that can be changed while open)
this.handleTemplateUpdate()
// Template transition phase events (handled once only)
// When the template has mounted, but not visibly shown yet
$tip.$once('show', this.onTemplateShow)
// When the template has completed showing
$tip.$once('shown', this.onTemplateShown)
// When the template has started to hide
$tip.$once('hide', this.onTemplateHide)
// When the template has completed hiding
$tip.$once('hidden', this.onTemplateHidden)
// When the template gets destroyed for any reason
$tip.$once('hook:destroyed', this.destroyTemplate)
// Convenience events from template
// To save us from manually adding/removing DOM
// listeners to tip element when it is open
$tip.$on('focusin', this.handleEvent)
$tip.$on('focusout', this.handleEvent)
$tip.$on('mouseenter', this.handleEvent)
$tip.$on('mouseleave', this.handleEvent)
// Mount (which triggers the `show`)
$tip.$mount(container.appendChild(document.createElement('div')))
// Template will automatically remove its markup from DOM when hidden
},
hideTemplate() {
// Trigger the template to start hiding
// The template will emit the `hide` event after this and
// then emit the `hidden` event once it is fully hidden
// The `hook:destroyed` will also be called (safety measure)
this.$_tip && this.$_tip.hide()
// Clear out any stragging active triggers
this.clearActiveTriggers()
// Reset the hover state
this.$_hoverState = ''
},
// Destroy the template instance and reset state
destroyTemplate() {
this.setWhileOpenListeners(false)
this.clearHoverTimeout()
this.$_hoverState = ''
this.clearActiveTriggers()
this.localPlacementTarget = null
try {
this.$_tip && this.$_tip.$destroy()
} catch {}
this.$_tip = null
this.removeAriaDescribedby()
this.restoreTitle()
this.localShow = false
},
getTemplateElement() {
return this.$_tip ? this.$_tip.$el : null
},
handleTemplateUpdate() {
// Update our template title/content "props"
// So that the template updates accordingly
const $tip = this.$_tip
if ($tip) {
const props = ['title', 'content', 'variant', 'customClass', 'noFade', 'interactive']
// Only update the values if they have changed
props.forEach(prop => {
if ($tip[prop] !== this[prop]) {
$tip[prop] = this[prop]
}
})
}
},
// --- Show/Hide handlers ---
// Show the tooltip
show() {
const target = this.getTarget()
if (
!target ||
!contains(document.body, target) ||
!isVisible(target) ||
this.dropdownOpen() ||
((isUndefinedOrNull(this.title) || this.title === '') &&
(isUndefinedOrNull(this.content) || this.content === ''))
) {
// If trigger element isn't in the DOM or is not visible, or
// is on an open dropdown toggle, or has no content, then
// we exit without showing
return
}
// If tip already exists, exit early
if (this.$_tip || this.localShow) {
/* istanbul ignore next */
return
}
// In the process of showing
this.localShow = true
// Create a cancelable BvEvent
const showEvt = this.buildEvent('show', { cancelable: true })
this.emitEvent(showEvt)
// Don't show if event cancelled
/* istanbul ignore next: ignore for now */
if (showEvt.defaultPrevented) {
// Destroy the template (if for some reason it was created)
/* istanbul ignore next */
this.destroyTemplate()
/* istanbul ignore next */
return
}
// Fix the title attribute on target
this.fixTitle()
// Set aria-describedby on target
this.addAriaDescribedby()
// Create and show the tooltip
this.createTemplateAndShow()
},
hide(force = false) {
// Hide the tooltip
const tip = this.getTemplateElement()
if (!tip || !this.localShow) {
/* istanbul ignore next */
this.restoreTitle()
/* istanbul ignore next */
return
}
// Emit cancelable BvEvent 'hide'
// We disable cancelling if `force` is true
const hideEvt = this.buildEvent('hide', { cancelable: !force })
this.emitEvent(hideEvt)
/* istanbul ignore next: ignore for now */
if (hideEvt.defaultPrevented) {
// Don't hide if event cancelled
/* istanbul ignore next */
return
}
// Tell the template to hide
this.hideTemplate()
},
forceHide() {
// Forcefully hides/destroys the template, regardless of any active triggers
const tip = this.getTemplateElement()
if (!tip || !this.localShow) {
/* istanbul ignore next */
return
}
// Disable while open listeners/watchers
// This is also done in the template `hide` evt handler
this.setWhileOpenListeners(false)
// Clear any hover enter/leave event
this.clearHoverTimeout()
this.$_hoverState = ''
this.clearActiveTriggers()
// Disable the fade animation on the template
if (this.$_tip) {
this.$_tip.noFade = true
}
// Hide the tip (with force = true)
this.hide(true)
},
enable() {
this.$_enabled = true
// Create a non-cancelable BvEvent
this.emitEvent(this.buildEvent('enabled'))
},
disable() {
this.$_enabled = false
// Create a non-cancelable BvEvent
this.emitEvent(this.buildEvent('disabled'))
},
// --- Handlers for template events ---
// When template is inserted into DOM, but not yet shown
onTemplateShow() {
// Enable while open listeners/watchers
this.setWhileOpenListeners(true)
},
// When template show transition completes
onTemplateShown() {
const prevHoverState = this.$_hoverState
this.$_hoverState = ''
if (prevHoverState === 'out') {
this.leave(null)
}
// Emit a non-cancelable BvEvent 'shown'
this.emitEvent(this.buildEvent('shown'))
},
// When template is starting to hide
onTemplateHide() {
// Disable while open listeners/watchers
this.setWhileOpenListeners(false)
},
// When template has completed closing (just before it self destructs)
onTemplateHidden() {
// Destroy the template
this.destroyTemplate()
// Emit a non-cancelable BvEvent 'shown'
this.emitEvent(this.buildEvent('hidden', {}))
},
// --- Utility methods ---
getTarget() {
// Handle case where target may be a component ref
let target = this.target ? this.target.$el || this.target : null
// If an ID
target = isString(target) ? getById(target.replace(/^#/, '')) : target
// If a function
target = isFunction(target) ? target() : target
// If an element ref
return isElement(target) ? target : null
},
getPlacementTarget() {
// This is the target that the tooltip will be placed on, which may not
// necessarily be the same element that has the trigger event listeners
// For now, this is the same as target
// TODO:
// Add in child selector support
// Add in visibility checks for this element
// Fallback to target if not found
return this.getTarget()
},
getTargetId() {
// Returns the ID of the trigger element
const target = this.getTarget()
return target && target.id ? target.id : null
},
getContainer() {
// Handle case where container may be a component ref
const container = this.container ? this.container.$el || this.container : false
const body = document.body
const target = this.getTarget()
// If we are in a modal, we append to the modal instead
// of body, unless a container is specified
// TODO:
// Template should periodically check to see if it is in dom
// And if not, self destruct (if container got v-if'ed out of DOM)
// Or this could possibly be part of the visibility check
return container === false
? closest(MODAL_SELECTOR, target) || body
: isString(container)
? getById(container.replace(/^#/, '')) || body
: body
},
getBoundary() {
return this.boundary ? this.boundary.$el || this.boundary : 'scrollParent'
},
isInModal() {
const target = this.getTarget()
return target && closest(MODAL_SELECTOR, target)
},
isDropdown() {
// Returns true if trigger is a dropdown
const target = this.getTarget()
return target && hasClass(target, DROPDOWN_CLASS)
},
dropdownOpen() {
// Returns true if trigger is a dropdown and the dropdown menu is open
const target = this.getTarget()
return this.isDropdown() && target && select(DROPDOWN_OPEN_SELECTOR, target)
},
clearHoverTimeout() {
if (this.$_hoverTimeout) {
clearTimeout(this.$_hoverTimeout)
this.$_hoverTimeout = null
}
},
clearVisibilityInterval() {
if (this.$_visibleInterval) {
clearInterval(this.$_visibleInterval)
this.$_visibleInterval = null
}
},
clearActiveTriggers() {
for (const trigger in this.activeTrigger) {
this.activeTrigger[trigger] = false
}
},
addAriaDescribedby() {
// Add aria-describedby on trigger element, without removing any other IDs
const target = this.getTarget()
let desc = getAttr(target, 'aria-describedby') || ''
desc = desc
.split(/\s+/)
.concat(this.computedId)
.join(' ')
.trim()
// Update/add aria-described by
setAttr(target, 'aria-describedby', desc)
},
removeAriaDescribedby() {
// Remove aria-describedby on trigger element, without removing any other IDs
const target = this.getTarget()
let desc = getAttr(target, 'aria-describedby') || ''
desc = desc
.split(/\s+/)
.filter(d => d !== this.computedId)
.join(' ')
.trim()
// Update or remove aria-describedby
if (desc) {
/* istanbul ignore next */
setAttr(target, 'aria-describedby', desc)
} else {
removeAttr(target, 'aria-describedby')
}
},
fixTitle() {
// If the target has a title attribute, null it out and
// store on data-title
const target = this.getTarget()
if (target && getAttr(target, 'title')) {
// We only update title attribute if it has a value
setAttr(target, 'data-original-title', getAttr(target, 'title') || '')
setAttr(target, 'title', '')
}
},
restoreTitle() {
// If target had a title, restore the title attribute
// and remove the data-title attribute
const target = this.getTarget()
if (target && hasAttr(target, 'data-original-title')) {
setAttr(target, 'title', getAttr(target, 'data-original-title') || '')
removeAttr(target, 'data-original-title')
}
},
// --- BvEvent helpers ---
buildEvent(type, opts = {}) {
// Defaults to a non-cancellable event
return new BvEvent(type, {
cancelable: false,
target: this.getTarget(),
relatedTarget: this.getTemplateElement() || null,
componentId: this.computedId,
vueTarget: this,
// Add in option overrides
...opts
})
},
emitEvent(bvEvt) {
// Emits a BvEvent on $root and this instance
const evtName = bvEvt.type
const $root = this.$root
if ($root && $root.$emit) {
// Emit an event on $root
$root.$emit(`bv::${this.templateType}::${evtName}`, bvEvt)
}
this.$emit(evtName, bvEvt)
},
// --- Event handler setup methods ---
listen() {
// Enable trigger event handlers
const el = this.getTarget()
if (!el) {
/* istanbul ignore next */
return
}
// Listen for global show/hide events
this.setRootListener(true)
// Set up our listeners on the target trigger element
this.computedTriggers.forEach(trigger => {
if (trigger === 'click') {
eventOn(el, 'click', this.handleEvent, EvtOpts)
} else if (trigger === 'focus') {
eventOn(el, 'focusin', this.handleEvent, EvtOpts)
eventOn(el, 'focusout', this.handleEvent, EvtOpts)
} else if (trigger === 'blur') {
// Used to close $tip when element looses focus
/* istanbul ignore next */
eventOn(el, 'focusout', this.handleEvent, EvtOpts)
} else if (trigger === 'hover') {
eventOn(el, 'mouseenter', this.handleEvent, EvtOpts)
eventOn(el, 'mouseleave', this.handleEvent, EvtOpts)
}
}, this)
},
unListen() /* istanbul ignore next */ {
// Remove trigger event handlers
const events = ['click', 'focusin', 'focusout', 'mouseenter', 'mouseleave']
const target = this.getTarget()
// Stop listening for global show/hide/enable/disable events
this.setRootListener(false)
// Clear out any active target listeners
events.forEach(evt => {
target && eventOff(target, evt, this.handleEvent, EvtOpts)
}, this)
},
setRootListener(on) {
// Listen for global `bv::{hide|show}::{tooltip|popover}` hide request event
const $root = this.$root
if ($root) {
const method = on ? '$on' : '$off'
const type = this.templateType
$root[method](`bv::hide::${type}`, this.doHide)
$root[method](`bv::show::${type}`, this.doShow)
$root[method](`bv::disable::${type}`, this.doDisable)
$root[method](`bv::enable::${type}`, this.doEnable)
}
},
setWhileOpenListeners(on) {
// Events that are only registered when the template is showing
// Modal close events
this.setModalListener(on)
// Dropdown open events (if we are attached to a dropdown)
this.setDropdownListener(on)
// Periodic $element visibility check
// For handling when tip target is in <keepalive>, tabs, carousel, etc
this.visibleCheck(on)
// On-touch start listeners
this.setOnTouchStartListener(on)
},
// Handler for periodic visibility check
visibleCheck(on) {
this.clearVisibilityInterval()
const target = this.getTarget()
const tip = this.getTemplateElement()
if (on) {
this.$_visibleInterval = setInterval(() => {
if (tip && this.localShow && (!target.parentNode || !isVisible(target))) {
// Target element is no longer visible or not in DOM, so force-hide the tooltip
this.forceHide()
}
}, 100)
}
},
setModalListener(on) {
// Handle case where tooltip/target is in a modal
if (this.isInModal()) {
// We can listen for modal hidden events on `$root`
this.$root[on ? '$on' : '$off'](MODAL_CLOSE_EVENT, this.forceHide)
}
},
setOnTouchStartListener(on) /* istanbul ignore next: JSDOM doesn't support `ontouchstart` */ {
// If this is a touch-enabled device we add extra empty
// `mouseover` listeners to the body's immediate children
// Only needed because of broken event delegation on iOS
// https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
if ('ontouchstart' in document.documentElement) {
const method = on ? eventOn : eventOff
arrayFrom(document.body.children).forEach(el => {
method(el, 'mouseover', this.$_noop)
})
}
},
setDropdownListener(on) {
const target = this.getTarget()
if (!target || !this.$root || !this.isDropdown) {
return
}
// We can listen for dropdown shown events on its instance
// TODO:
// We could grab the ID from the dropdown, and listen for
// $root events for that particular dropdown id
// Dropdown shown and hidden events will need to emit
// Note: Dropdown auto-ID happens in a `$nextTick()` after mount
// So the ID lookup would need to be done in a `$nextTick()`
if (target.__vue__) {
target.__vue__[on ? '$on' : '$off']('shown', this.forceHide)
}
},
// --- Event handlers ---
handleEvent(evt) {
// General trigger event handler
// target is the trigger element
const target = this.getTarget()
if (!target || isDisabled(target) || !this.$_enabled || this.dropdownOpen()) {
// If disabled or not enabled, or if a dropdown that is open, don't do anything
// If tip is shown before element gets disabled, then tip will not
// close until no longer disabled or forcefully closed
return
}
const type = evt.type
const triggers = this.computedTriggers
if (type === 'click' && arrayIncludes(triggers, 'click')) {
this.click(evt)
} else if (type === 'mouseenter' && arrayIncludes(triggers, 'hover')) {
// `mouseenter` is a non-bubbling event
this.enter(evt)
} else if (type === 'focusin' && arrayIncludes(triggers, 'focus')) {
// `focusin` is a bubbling event
// `evt` includes `relatedTarget` (element loosing focus)
this.enter(evt)
} else if (
(type === 'focusout' &&
(arrayIncludes(triggers, 'focus') || arrayIncludes(triggers, 'blur'))) ||
(type === 'mouseleave' && arrayIncludes(triggers, 'hover'))
) {
// `focusout` is a bubbling event
// `mouseleave` is a non-bubbling event
// `tip` is the template (will be null if not open)
const tip = this.getTemplateElement()
// `evtTarget` is the element which is loosing focus/hover and
const evtTarget = evt.target
// `relatedTarget` is the element gaining focus/hover
const relatedTarget = evt.relatedTarget
/* istanbul ignore next */
if (
// From tip to target
(tip && contains(tip, evtTarget) && contains(target, relatedTarget)) ||
// From target to tip
(tip && contains(target, evtTarget) && contains(tip, relatedTarget)) ||
// Within tip
(tip && contains(tip, evtTarget) && contains(tip, relatedTarget)) ||
// Within target
(contains(target, evtTarget) && contains(target, relatedTarget))
) {
// If focus/hover moves within `tip` and `target`, don't trigger a leave
return
}
// Otherwise trigger a leave
this.leave(evt)
}
},
doHide(id) {
// Programmatically hide tooltip or popover
if (!id || (this.getTargetId() === id || this.computedId === id)) {
// Close all tooltips or popovers, or this specific tip (with ID)
this.forceHide()
}
},
doShow(id) {
// Programmatically show tooltip or popover
if (!id || (this.getTargetId() === id || this.computedId === id)) {
// Open all tooltips or popovers, or this specific tip (with ID)
this.show()
}
},
doDisable(id) /*istanbul ignore next: ignore for now */ {
// Programmatically disable tooltip or popover
if (!id || (this.getTargetId() === id || this.computedId === id)) {
// Disable all tooltips or popovers (no ID), or this specific tip (with ID)
this.disable()
}
},
doEnable(id) /*istanbul ignore next: ignore for now */ {
// Programmatically enable tooltip or popover
if (!id || (this.getTargetId() === id || this.computedId === id)) {
// Enable all tooltips or popovers (no ID), or this specific tip (with ID)
this.enable()
}
},
click(evt) {
if (!this.$_enabled || this.dropdownOpen()) {
/* istanbul ignore next */
return
}
this.activeTrigger.click = !this.activeTrigger.click
if (this.isWithActiveTrigger) {
this.enter(null)
} else {
/* istanbul ignore next */
this.leave(null)
}
},
toggle() /* istanbul ignore next */ {
// Manual toggle handler
if (!this.$_enabled || this.dropdownOpen()) {
/* istanbul ignore next */
return
}
// Should we register as an active trigger?
// this.activeTrigger.manual = !this.activeTrigger.manual
if (this.localShow) {
this.leave(null)
} else {
this.enter(null)
}
},
enter(evt = null) {
// Opening trigger handler
// Note: Click events are sent with evt === null
if (evt) {
this.activeTrigger[evt.type === 'focusin' ? 'focus' : 'hover'] = true
}
/* istanbul ignore next */
if (this.localShow || this.$_hoverState === 'in') {
this.$_hoverState = 'in'
return
}
this.clearHoverTimeout()
this.$_hoverState = 'in'
if (!this.computedDelay.show) {
this.show()
} else {
// Hide any title attribute while enter delay is active
this.fixTitle()
this.$_hoverTimeout = setTimeout(() => {
/* istanbul ignore else */
if (this.$_hoverState === 'in') {
this.show()
} else if (!this.localShow) {
this.restoreTitle()
}
}, this.computedDelay.show)
}
},
leave(evt = null) {
// Closing trigger handler
// Note: Click events are sent with evt === null
if (evt) {
this.activeTrigger[evt.type === 'focusout' ? 'focus' : 'hover'] = false
/* istanbul ignore next */
if (evt.type === 'focusout' && arrayIncludes(this.computedTriggers, 'blur')) {
// Special case for `blur`: we clear out the other triggers
this.activeTrigger.click = false
this.activeTrigger.hover = false
}
}
/* istanbul ignore next: ignore for now */
if (this.isWithActiveTrigger) {
return
}
this.clearHoverTimeout()
this.$_hoverState = 'out'
if (!this.computedDelay.hide) {
this.hide()
} else {
this.$_hoverTimeout = setTimeout(() => {
if (this.$_hoverState === 'out') {
this.hide()
}
}, this.computedDelay.hide)
}
}
}
})