UNPKG

bootstrap-vue

Version:

BootstrapVue, with over 40 plugins and more than 75 custom components, provides one of the most comprehensive implementations of Bootstrap v4 components and grid system for Vue.js. With extensive and automated WAI-ARIA accessibility markup.

343 lines (337 loc) 9.77 kB
/* * Tooltip/Popover component mixin * Common props */ import observeDom from '../utils/observe-dom' import { isElement, getById } from '../utils/dom' import { isArray, isFunction, isObject, isString } from '../utils/inspect' import { HTMLElement } from '../utils/safe-types' // --- Constants --- const PLACEMENTS = { top: 'top', topleft: 'topleft', topright: 'topright', right: 'right', righttop: 'righttop', rightbottom: 'rightbottom', bottom: 'bottom', bottomleft: 'bottomleft', bottomright: 'bottomright', left: 'left', lefttop: 'lefttop', leftbottom: 'leftbottom', auto: 'auto' } const OBSERVER_CONFIG = { subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['class', 'style'] } // @vue/component export default { props: { target: { // String ID of element, or element/component reference type: [String, Object, HTMLElement, Function] // default: undefined }, offset: { type: [Number, String], default: 0 }, noFade: { type: Boolean, default: false }, container: { // String ID of container, if null body is used (default) type: String, default: null }, show: { type: Boolean, default: false }, disabled: { type: Boolean, default: false } }, data() { return { // semaphore for preventing multiple show events localShow: false } }, computed: { baseConfig() { const cont = this.container let delay = isObject(this.delay) ? this.delay : parseInt(this.delay, 10) || 0 return { // Title prop title: (this.title || '').trim() || '', // Content prop (if popover) content: (this.content || '').trim() || '', // Tooltip/Popover placement placement: PLACEMENTS[this.placement] || 'auto', // Tooltip/popover fallback placemenet fallbackPlacement: this.fallbackPlacement || 'flip', // Container currently needs to be an ID with '#' prepended, if null then body is used container: cont ? (/^#/.test(cont) ? cont : `#${cont}`) : false, // boundariesElement passed to popper boundary: this.boundary, // boundariesElement padding passed to popper boundaryPadding: this.boundaryPadding, // Show/Hide delay delay: delay || 0, // Offset can be css distance. if no units, pixels are assumed offset: this.offset || 0, // Disable fade Animation? animation: !this.noFade, // Open/Close Trigger(s) trigger: isArray(this.triggers) ? this.triggers.join(' ') : this.triggers, // Callbacks so we can trigger events on component callbacks: { show: this.onShow, shown: this.onShown, hide: this.onHide, hidden: this.onHidden, enabled: this.onEnabled, disabled: this.onDisabled } } } }, watch: { show(show, old) { if (show !== old) { show ? this.onOpen() : this.onClose() } }, disabled(disabled, old) { if (disabled !== old) { disabled ? this.onDisable() : this.onEnable() } }, localShow(show, old) { if (show !== this.show) { this.$emit('update:show', show) } } }, created() { // Create non-reactive property this._toolpop = null this._obs_title = null this._obs_content = null }, mounted() { // We do this in a next tick to ensure DOM has rendered first this.$nextTick(() => { // Instantiate ToolTip/PopOver on target // The createToolpop method must exist in main component if (this.createToolpop()) { if (this.disabled) { // Initially disabled this.onDisable() } // Listen to open signals from others this.$on('open', this.onOpen) // Listen to close signals from others this.$on('close', this.onClose) // Listen to disable signals from others this.$on('disable', this.onDisable) // Listen to enable signals from others this.$on('enable', this.onEnable) // Observe content Child changes so we can notify popper of possible size change this.setObservers(true) // Set initially open state if (this.show) { this.onOpen() } } }) }, updated() { // If content/props changes, etc if (this._toolpop) { this._toolpop.updateConfig(this.getConfig()) } }, activated() /* istanbul ignore next: can't easily test in JSDOM */ { // Called when component is inside a <keep-alive> and component brought offline this.setObservers(true) }, deactivated() /* istanbul ignore next: can't easily test in JSDOM */ { // Called when component is inside a <keep-alive> and component taken offline if (this._toolpop) { this.setObservers(false) this._toolpop.hide() } }, beforeDestroy() { // Shutdown our local event listeners this.$off('open', this.onOpen) this.$off('close', this.onClose) this.$off('disable', this.onDisable) this.$off('enable', this.onEnable) this.setObservers(false) // bring our content back if needed this.bringItBack() if (this._toolpop) { this._toolpop.destroy() this._toolpop = null } }, methods: { getConfig() { const cfg = { ...this.baseConfig } if (this.$refs.title && this.$refs.title.innerHTML.trim()) { // If slot has content, it overrides 'title' prop // We use the DOM node as content to allow components! cfg.title = this.$refs.title cfg.html = true } if (this.$refs.content && this.$refs.content.innerHTML.trim()) { // If slot has content, it overrides 'content' prop // We use the DOM node as content to allow components! cfg.content = this.$refs.content cfg.html = true } return cfg }, onOpen() { if (this._toolpop && !this.localShow) { this.localShow = true this._toolpop.show() } }, onClose(callback) { // What is callback for ? it is not documented /* istanbul ignore else */ if (this._toolpop && this.localShow) { this._toolpop.hide(callback) } else if (isFunction(callback)) { // Is this even used? callback() } }, onDisable() { if (this._toolpop) { this._toolpop.disable() } }, onEnable() { if (this._toolpop) { this._toolpop.enable() } }, updatePosition() { /* istanbul ignore next: can't test in JSDOM until mutation observer is implemented */ if (this._toolpop) { // Instruct popper to reposition popover if necessary this._toolpop.update() } }, getTarget() { let target = this.target if (isFunction(target)) { /* istanbul ignore next */ target = target() } /* istanbul ignore else */ if (isString(target)) { // Assume ID of element return getById(target) } else if (isObject(target) && isElement(target.$el)) { // Component reference /* istanbul ignore next */ return target.$el } else if (isObject(target) && isElement(target)) { // Element reference /* istanbul ignore next */ return target } /* istanbul ignore next */ return null }, // Callbacks called by Tooltip/Popover class instance onShow(evt) { this.$emit('show', evt) this.localShow = !(evt && evt.defaultPrevented) }, onShown(evt) { this.setObservers(true) this.$emit('shown', evt) this.localShow = true }, onHide(evt) { this.$emit('hide', evt) this.localShow = !!(evt && evt.defaultPrevented) }, onHidden(evt) { this.setObservers(false) // bring our content back if needed to keep Vue happy // Tooltip class will move it back to tip when shown again this.bringItBack() this.$emit('hidden', evt) this.localShow = false }, onEnabled(evt) { /* istanbul ignore next */ if (!evt || evt.type !== 'enabled') { // Prevent possible endless loop if user mistakenly fires enabled instead of enable return } this.$emit('update:disabled', false) this.$emit('disabled') }, onDisabled(evt) { /* istanbul ignore next */ if (!evt || evt.type !== 'disabled') { // Prevent possible endless loop if user mistakenly fires disabled instead of disable return } this.$emit('update:disabled', true) this.$emit('enabled') }, bringItBack() { // bring our content back if needed to keep Vue happy if (this.$el && this.$refs.title) { this.$el.appendChild(this.$refs.title) } if (this.$el && this.$refs.content) { this.$el.appendChild(this.$refs.content) } }, setObservers(on) { if (on) { if (this.$refs.title) { this._obs_title = observeDom( this.$refs.title, this.updatePosition.bind(this), OBSERVER_CONFIG ) } if (this.$refs.content) { this._obs_content = observeDom( this.$refs.content, this.updatePosition.bind(this), OBSERVER_CONFIG ) } } else { if (this._obs_title) { this._obs_title.disconnect() this._obs_title = null } if (this._obs_content) { this._obs_content.disconnect() this._obs_content = null } } } } }