UNPKG

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

269 lines (250 loc) 7.98 kB
import getScopId from '../../utils/get-scope-id' import identity from '../../utils/identity' import looseEqual from '../../utils/loose-equal' import { concat } from '../../utils/array' import { getComponentConfig } from '../../utils/config' import { isBrowser } from '../../utils/env' import { isFunction, isNumber, isPlainObject, isString, isUndefined, isUndefinedOrNull } from '../../utils/inspect' import { keys } from '../../utils/object' import { BVTooltip } from '../../components/tooltip/helpers/bv-tooltip' // Key which we use to store tooltip object on element const BV_TOOLTIP = '__BV_Tooltip__' // Default trigger const DefaultTrigger = 'hover focus' // Valid event triggers const validTriggers = { focus: true, hover: true, click: true, blur: true, manual: true } // Directive modifier test regular expressions. Pre-compile for performance const htmlRE = /^html$/i const noninteractiveRE = /^noninteractive$/i const noFadeRE = /^nofade$/i const placementRE = /^(auto|top(left|right)?|bottom(left|right)?|left(top|bottom)?|right(top|bottom)?)$/i const boundaryRE = /^(window|viewport|scrollParent)$/i const delayRE = /^d\d+$/i const delayShowRE = /^ds\d+$/i const delayHideRE = /^dh\d+$/i const offsetRE = /^o-?\d+$/i const variantRE = /^v-.+$/i const spacesRE = /\s+/ // Build a Tooltip config based on bindings (if any) // Arguments and modifiers take precedence over passed value config object const parseBindings = (bindings, vnode) => /* istanbul ignore next: not easy to test */ { // We start out with a basic config const NAME = 'BTooltip' // Default config let config = { title: undefined, trigger: '', // Default set below if needed placement: 'top', fallbackPlacement: 'flip', container: false, // Default of body animation: true, offset: 0, id: null, html: false, interactive: true, disabled: false, delay: getComponentConfig(NAME, 'delay'), boundary: String(getComponentConfig(NAME, 'boundary')), boundaryPadding: parseInt(getComponentConfig(NAME, 'boundaryPadding'), 10) || 0, variant: getComponentConfig(NAME, 'variant'), customClass: getComponentConfig(NAME, 'customClass') } // Process `bindings.value` if (isString(bindings.value) || isNumber(bindings.value)) { // Value is tooltip content (HTML optionally supported) config.title = bindings.value } else if (isFunction(bindings.value)) { // Title generator function config.title = bindings.value } else if (isPlainObject(bindings.value)) { // Value is config object, so merge config = { ...config, ...bindings.value } } // If title is not provided, try title attribute if (isUndefined(config.title)) { // Try attribute const data = vnode.data || {} config.title = data.attrs && !isUndefinedOrNull(data.attrs.title) ? data.attrs.title : undefined } // Normalize delay if (!isPlainObject(config.delay)) { config.delay = { show: parseInt(config.delay, 10) || 0, hide: parseInt(config.delay, 10) || 0 } } // If argument, assume element ID of container element if (bindings.arg) { // Element ID specified as arg // We must prepend '#' to become a CSS selector config.container = `#${bindings.arg}` } // Process modifiers keys(bindings.modifiers).forEach(mod => { if (htmlRE.test(mod)) { // Title allows HTML config.html = true } else if (noninteractiveRE.test(mod)) { // Noninteractive config.interactive = false } else if (noFadeRE.test(mod)) { // No animation config.animation = false } else if (placementRE.test(mod)) { // Placement of tooltip config.placement = mod } else if (boundaryRE.test(mod)) { // Boundary of tooltip mod = mod === 'scrollparent' ? 'scrollParent' : mod config.boundary = mod } else if (delayRE.test(mod)) { // Delay value const delay = parseInt(mod.slice(1), 10) || 0 config.delay.show = delay config.delay.hide = delay } else if (delayShowRE.test(mod)) { // Delay show value config.delay.show = parseInt(mod.slice(2), 10) || 0 } else if (delayHideRE.test(mod)) { // Delay hide value config.delay.hide = parseInt(mod.slice(2), 10) || 0 } else if (offsetRE.test(mod)) { // Offset value, negative allowed config.offset = parseInt(mod.slice(1), 10) || 0 } else if (variantRE.test(mod)) { // Variant config.variant = mod.slice(2) || null } }) // Special handling of event trigger modifiers trigger is // a space separated list const selectedTriggers = {} // Parse current config object trigger concat(config.trigger || '') .filter(identity) .join(' ') .trim() .toLowerCase() .split(spacesRE) .forEach(trigger => { if (validTriggers[trigger]) { selectedTriggers[trigger] = true } }) // Parse modifiers for triggers keys(bindings.modifiers).forEach(mod => { mod = mod.toLowerCase() if (validTriggers[mod]) { // If modifier is a valid trigger selectedTriggers[mod] = true } }) // Sanitize triggers config.trigger = keys(selectedTriggers).join(' ') if (config.trigger === 'blur') { // Blur by itself is useless, so convert it to 'focus' config.trigger = 'focus' } if (!config.trigger) { // Use default trigger config.trigger = DefaultTrigger } // Return the config return config } // Add/update Tooltip on our element const applyTooltip = (el, bindings, vnode) => { if (!isBrowser) { /* istanbul ignore next */ return } const config = parseBindings(bindings, vnode) if (!el[BV_TOOLTIP]) { const $parent = vnode.context el[BV_TOOLTIP] = new BVTooltip({ parent: $parent, // Add the parent's scoped style attribute data _scopeId: getScopId($parent, undefined) }) el[BV_TOOLTIP].__bv_prev_data__ = {} el[BV_TOOLTIP].$on('show', () => /* istanbul ignore next: for now */ { // Before showing the tooltip, we update the title if it is a function if (isFunction(config.title)) { el[BV_TOOLTIP].updateData({ title: config.title(el) }) } }) } const data = { title: config.title, triggers: config.trigger, placement: config.placement, fallbackPlacement: config.fallbackPlacement, variant: config.variant, customClass: config.customClass, container: config.container, boundary: config.boundary, delay: config.delay, offset: config.offset, noFade: !config.animation, id: config.id, interactive: config.interactive, disabled: config.disabled, html: config.html } const oldData = el[BV_TOOLTIP].__bv_prev_data__ el[BV_TOOLTIP].__bv_prev_data__ = data if (!looseEqual(data, oldData)) { // We only update the instance if data has changed const newData = { target: el } keys(data).forEach(prop => { // We only pass data properties that have changed if (data[prop] !== oldData[prop]) { // if title is a function, we execute it here newData[prop] = prop === 'title' && isFunction(data[prop]) ? data[prop](el) : data[prop] } }) el[BV_TOOLTIP].updateData(newData) } } // Remove Tooltip on our element const removeTooltip = el => { if (el[BV_TOOLTIP]) { el[BV_TOOLTIP].$destroy() el[BV_TOOLTIP] = null } delete el[BV_TOOLTIP] } // Export our directive export const VBTooltip = { bind(el, bindings, vnode) { applyTooltip(el, bindings, vnode) }, // We use `componentUpdated` here instead of `update`, as the former // waits until the containing component and children have finished updating componentUpdated(el, bindings, vnode) { // Performed in a `$nextTick()` to prevent render update loops vnode.context.$nextTick(() => { applyTooltip(el, bindings, vnode) }) }, unbind(el) { removeTooltip(el) } }