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
317 lines (309 loc) • 10.1 kB
JavaScript
import { extend } from '../../vue'
import { NAME_TOOLTIP } from '../../constants/components'
import {
EVENT_NAME_CLOSE,
EVENT_NAME_DISABLE,
EVENT_NAME_DISABLED,
EVENT_NAME_ENABLE,
EVENT_NAME_ENABLED,
EVENT_NAME_HIDDEN,
EVENT_NAME_HIDE,
EVENT_NAME_OPEN,
EVENT_NAME_SHOW,
EVENT_NAME_SHOWN,
MODEL_EVENT_NAME_PREFIX
} from '../../constants/events'
import {
PROP_TYPE_ARRAY_STRING,
PROP_TYPE_BOOLEAN,
PROP_TYPE_FUNCTION,
PROP_TYPE_NUMBER_OBJECT_STRING,
PROP_TYPE_NUMBER_STRING,
PROP_TYPE_OBJECT,
PROP_TYPE_STRING
} from '../../constants/props'
import { HTMLElement, SVGElement } from '../../constants/safe-types'
import { useParentMixin } from '../../mixins/use-parent'
import { getScopeId } from '../../utils/get-scope-id'
import { isUndefinedOrNull } from '../../utils/inspect'
import { pick } from '../../utils/object'
import { makeProp, makePropsConfigurable } from '../../utils/props'
import { createNewChildComponent } from '../../utils/create-new-child-component'
import { normalizeSlotMixin } from '../../mixins/normalize-slot'
import { BVTooltip } from './helpers/bv-tooltip'
// --- Constants ---
const MODEL_PROP_NAME_ENABLED = 'disabled'
const MODEL_EVENT_NAME_ENABLED = MODEL_EVENT_NAME_PREFIX + MODEL_PROP_NAME_ENABLED
const MODEL_PROP_NAME_SHOW = 'show'
const MODEL_EVENT_NAME_SHOW = MODEL_EVENT_NAME_PREFIX + MODEL_PROP_NAME_SHOW
// --- Props ---
export const props = makePropsConfigurable(
{
// String: scrollParent, window, or viewport
// Element: element reference
// Object: Vue component
boundary: makeProp([HTMLElement, PROP_TYPE_OBJECT, PROP_TYPE_STRING], 'scrollParent'),
boundaryPadding: makeProp(PROP_TYPE_NUMBER_STRING, 50),
// String: HTML ID of container, if null body is used (default)
// HTMLElement: element reference reference
// Object: Vue Component
container: makeProp([HTMLElement, PROP_TYPE_OBJECT, PROP_TYPE_STRING]),
customClass: makeProp(PROP_TYPE_STRING),
delay: makeProp(PROP_TYPE_NUMBER_OBJECT_STRING, 50),
[MODEL_PROP_NAME_ENABLED]: makeProp(PROP_TYPE_BOOLEAN, false),
fallbackPlacement: makeProp(PROP_TYPE_ARRAY_STRING, 'flip'),
// ID to use for tooltip element
// If not provided on will automatically be generated
id: makeProp(PROP_TYPE_STRING),
noFade: makeProp(PROP_TYPE_BOOLEAN, false),
noninteractive: makeProp(PROP_TYPE_BOOLEAN, false),
offset: makeProp(PROP_TYPE_NUMBER_STRING, 0),
placement: makeProp(PROP_TYPE_STRING, 'top'),
[MODEL_PROP_NAME_SHOW]: makeProp(PROP_TYPE_BOOLEAN, false),
// String ID of element, or element/component reference
// Or function that returns one of the above
// Required
target: makeProp(
[HTMLElement, SVGElement, PROP_TYPE_FUNCTION, PROP_TYPE_OBJECT, PROP_TYPE_STRING],
undefined,
true
),
title: makeProp(PROP_TYPE_STRING),
triggers: makeProp(PROP_TYPE_ARRAY_STRING, 'hover focus'),
variant: makeProp(PROP_TYPE_STRING)
},
NAME_TOOLTIP
)
// --- Main component ---
// @vue/component
export const BTooltip = /*#__PURE__*/ extend({
name: NAME_TOOLTIP,
mixins: [normalizeSlotMixin, useParentMixin],
inheritAttrs: false,
props,
data() {
return {
localShow: this[MODEL_PROP_NAME_SHOW],
localTitle: '',
localContent: ''
}
},
computed: {
// Data that will be passed to the template and popper
templateData() {
return {
title: this.localTitle,
content: this.localContent,
interactive: !this.noninteractive,
// Pass these props as is
...pick(this.$props, [
'boundary',
'boundaryPadding',
'container',
'customClass',
'delay',
'fallbackPlacement',
'id',
'noFade',
'offset',
'placement',
'target',
'target',
'triggers',
'variant',
MODEL_PROP_NAME_ENABLED
])
}
},
// Used to watch for changes to the title and content props
templateTitleContent() {
const { title, content } = this
return { title, content }
}
},
watch: {
[MODEL_PROP_NAME_SHOW](newValue, oldValue) {
if (newValue !== oldValue && newValue !== this.localShow && this.$_toolpop) {
if (newValue) {
this.$_toolpop.show()
} else {
// We use `forceHide()` to override any active triggers
this.$_toolpop.forceHide()
}
}
},
[MODEL_PROP_NAME_ENABLED](newValue) {
if (newValue) {
this.doDisable()
} else {
this.doEnable()
}
},
localShow(newValue) {
// TODO: May need to be done in a `$nextTick()`
this.$emit(MODEL_EVENT_NAME_SHOW, newValue)
},
templateData() {
this.$nextTick(() => {
if (this.$_toolpop) {
this.$_toolpop.updateData(this.templateData)
}
})
},
// Watchers for title/content props (prop changes do not trigger the `updated()` hook)
templateTitleContent() {
this.$nextTick(this.updateContent)
}
},
created() {
// Create private non-reactive props
this.$_toolpop = null
},
updated() {
// Update the `propData` object
// Done in a `$nextTick()` to ensure slot(s) have updated
this.$nextTick(this.updateContent)
},
beforeDestroy() {
// Shutdown our local event listeners
this.$off(EVENT_NAME_OPEN, this.doOpen)
this.$off(EVENT_NAME_CLOSE, this.doClose)
this.$off(EVENT_NAME_DISABLE, this.doDisable)
this.$off(EVENT_NAME_ENABLE, this.doEnable)
// Destroy the tip instance
if (this.$_toolpop) {
this.$_toolpop.$destroy()
this.$_toolpop = null
}
},
mounted() {
// Instantiate a new BVTooltip instance
// Done in a `$nextTick()` to ensure DOM has completed rendering
// so that target can be found
this.$nextTick(() => {
// Load the on demand child instance
const Component = this.getComponent()
// Ensure we have initial content
this.updateContent()
// Pass down the scoped style attribute if available
const scopeId = getScopeId(this) || getScopeId(this.bvParent)
// Create the instance
const $toolpop = (this.$_toolpop = createNewChildComponent(this, Component, {
// Pass down the scoped style ID
_scopeId: scopeId || undefined
}))
// Set the initial data
$toolpop.updateData(this.templateData)
// Set listeners
$toolpop.$on(EVENT_NAME_SHOW, this.onShow)
$toolpop.$on(EVENT_NAME_SHOWN, this.onShown)
$toolpop.$on(EVENT_NAME_HIDE, this.onHide)
$toolpop.$on(EVENT_NAME_HIDDEN, this.onHidden)
$toolpop.$on(EVENT_NAME_DISABLED, this.onDisabled)
$toolpop.$on(EVENT_NAME_ENABLED, this.onEnabled)
// Initially disabled?
if (this[MODEL_PROP_NAME_ENABLED]) {
// Initially disabled
this.doDisable()
}
// Listen to open signals from others
this.$on(EVENT_NAME_OPEN, this.doOpen)
// Listen to close signals from others
this.$on(EVENT_NAME_CLOSE, this.doClose)
// Listen to disable signals from others
this.$on(EVENT_NAME_DISABLE, this.doDisable)
// Listen to enable signals from others
this.$on(EVENT_NAME_ENABLE, this.doEnable)
// Initially show tooltip?
if (this.localShow) {
$toolpop.show()
}
})
},
methods: {
getComponent() {
// Overridden by BPopover
return BVTooltip
},
updateContent() {
// Overridden by BPopover
// Tooltip: Default slot is `title`
// Popover: Default slot is `content`, `title` slot is title
// We pass a scoped slot function reference by default (Vue v2.6x)
// And pass the title prop as a fallback
this.setTitle(this.normalizeSlot() || this.title)
},
// Helper methods for `updateContent()`
setTitle(value) {
value = isUndefinedOrNull(value) ? '' : value
// We only update the value if it has changed
if (this.localTitle !== value) {
this.localTitle = value
}
},
setContent(value) {
value = isUndefinedOrNull(value) ? '' : value
// We only update the value if it has changed
if (this.localContent !== value) {
this.localContent = value
}
},
// --- Template event handlers ---
onShow(bvEvent) {
// Placeholder
this.$emit(EVENT_NAME_SHOW, bvEvent)
if (bvEvent) {
this.localShow = !bvEvent.defaultPrevented
}
},
onShown(bvEvent) {
// Tip is now showing
this.localShow = true
this.$emit(EVENT_NAME_SHOWN, bvEvent)
},
onHide(bvEvent) {
this.$emit(EVENT_NAME_HIDE, bvEvent)
},
onHidden(bvEvent) {
// Tip is no longer showing
this.$emit(EVENT_NAME_HIDDEN, bvEvent)
this.localShow = false
},
onDisabled(bvEvent) {
// Prevent possible endless loop if user mistakenly
// fires `disabled` instead of `disable`
if (bvEvent && bvEvent.type === EVENT_NAME_DISABLED) {
this.$emit(MODEL_EVENT_NAME_ENABLED, true)
this.$emit(EVENT_NAME_DISABLED, bvEvent)
}
},
onEnabled(bvEvent) {
// Prevent possible endless loop if user mistakenly
// fires `enabled` instead of `enable`
if (bvEvent && bvEvent.type === EVENT_NAME_ENABLED) {
this.$emit(MODEL_EVENT_NAME_ENABLED, false)
this.$emit(EVENT_NAME_ENABLED, bvEvent)
}
},
// --- Local event listeners ---
doOpen() {
!this.localShow && this.$_toolpop && this.$_toolpop.show()
},
doClose() {
this.localShow && this.$_toolpop && this.$_toolpop.hide()
},
doDisable() {
this.$_toolpop && this.$_toolpop.disable()
},
doEnable() {
this.$_toolpop && this.$_toolpop.enable()
}
},
render(h) {
// Always renders a comment node
// TODO:
// Future: Possibly render a target slot (single root element)
// which we can apply the listeners to (pass `this.$el` to BVTooltip)
return h()
}
})