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

249 lines (239 loc) 7.46 kB
// Base on-demand component for tooltip / popover templates // // Currently: // Responsible for positioning and transitioning the template // Templates are only instantiated when shown, and destroyed when hidden // import Popper from 'popper.js' import { extend } from '../../../vue' import { NAME_POPPER } from '../../../constants/components' import { EVENT_NAME_HIDDEN, EVENT_NAME_HIDE, EVENT_NAME_SHOW, EVENT_NAME_SHOWN, HOOK_EVENT_NAME_DESTROYED } from '../../../constants/events' import { PROP_TYPE_ARRAY_STRING, PROP_TYPE_NUMBER_STRING, PROP_TYPE_STRING } from '../../../constants/props' import { HTMLElement, SVGElement } from '../../../constants/safe-types' import { useParentMixin } from '../../../mixins/use-parent' import { getCS, requestAF, select } from '../../../utils/dom' import { toFloat } from '../../../utils/number' import { makeProp } from '../../../utils/props' import { BVTransition } from '../../transition/bv-transition' // --- Constants --- const AttachmentMap = { AUTO: 'auto', TOP: 'top', RIGHT: 'right', BOTTOM: 'bottom', LEFT: 'left', TOPLEFT: 'top', TOPRIGHT: 'top', RIGHTTOP: 'right', RIGHTBOTTOM: 'right', BOTTOMLEFT: 'bottom', BOTTOMRIGHT: 'bottom', LEFTTOP: 'left', LEFTBOTTOM: 'left' } const OffsetMap = { AUTO: 0, TOPLEFT: -1, TOP: 0, TOPRIGHT: +1, RIGHTTOP: -1, RIGHT: 0, RIGHTBOTTOM: +1, BOTTOMLEFT: -1, BOTTOM: 0, BOTTOMRIGHT: +1, LEFTTOP: -1, LEFT: 0, LEFTBOTTOM: +1 } // --- Props --- export const props = { // The minimum distance (in `px`) from the edge of the // tooltip/popover that the arrow can be positioned arrowPadding: makeProp(PROP_TYPE_NUMBER_STRING, 6), // 'scrollParent', 'viewport', 'window', or `Element` boundary: makeProp([HTMLElement, PROP_TYPE_STRING], 'scrollParent'), // Tooltip/popover will try and stay away from // boundary edge by this many pixels boundaryPadding: makeProp(PROP_TYPE_NUMBER_STRING, 5), fallbackPlacement: makeProp(PROP_TYPE_ARRAY_STRING, 'flip'), offset: makeProp(PROP_TYPE_NUMBER_STRING, 0), placement: makeProp(PROP_TYPE_STRING, 'top'), // Element that the tooltip/popover is positioned relative to target: makeProp([HTMLElement, SVGElement]) } // --- Main component --- // @vue/component export const BVPopper = /*#__PURE__*/ extend({ name: NAME_POPPER, mixins: [useParentMixin], props, data() { return { // reactive props set by parent noFade: false, // State related data localShow: true, attachment: this.getAttachment(this.placement) } }, computed: { /* istanbul ignore next */ templateType() { // Overridden by template component return 'unknown' }, popperConfig() { const { placement } = this return { placement: this.getAttachment(placement), modifiers: { offset: { offset: this.getOffset(placement) }, flip: { behavior: this.fallbackPlacement }, // `arrow.element` can also be a reference to an HTML Element // maybe we should make this a `$ref` in the templates? arrow: { element: '.arrow' }, preventOverflow: { padding: this.boundaryPadding, boundariesElement: this.boundary } }, onCreate: data => { // Handle flipping arrow classes if (data.originalPlacement !== data.placement) { /* istanbul ignore next: can't test in JSDOM */ this.popperPlacementChange(data) } }, onUpdate: data => { // Handle flipping arrow classes this.popperPlacementChange(data) } } } }, created() { // Note: We are created on-demand, and should be guaranteed that // DOM is rendered/ready by the time the created hook runs this.$_popper = null // Ensure we show as we mount this.localShow = true // Create popper instance before shown this.$on(EVENT_NAME_SHOW, el => { this.popperCreate(el) }) // Self destruct handler const handleDestroy = () => { this.$nextTick(() => { // In a `requestAF()` to release control back to application requestAF(() => { this.$destroy() }) }) } // Self destruct if parent destroyed this.bvParent.$once(HOOK_EVENT_NAME_DESTROYED, handleDestroy) // Self destruct after hidden this.$once(EVENT_NAME_HIDDEN, handleDestroy) }, beforeMount() { // Ensure that the attachment position is correct before mounting // as our propsData is added after `new Template({...})` this.attachment = this.getAttachment(this.placement) }, updated() { // Update popper if needed // TODO: Should this be a watcher on `this.popperConfig` instead? this.updatePopper() }, beforeDestroy() { this.destroyPopper() }, destroyed() { // Make sure template is removed from DOM const el = this.$el el && el.parentNode && el.parentNode.removeChild(el) }, methods: { // "Public" method to trigger hide template hide() { this.localShow = false }, // Private getAttachment(placement) { return AttachmentMap[String(placement).toUpperCase()] || 'auto' }, getOffset(placement) { if (!this.offset) { // Could set a ref for the arrow element const arrow = this.$refs.arrow || select('.arrow', this.$el) const arrowOffset = toFloat(getCS(arrow).width, 0) + toFloat(this.arrowPadding, 0) switch (OffsetMap[String(placement).toUpperCase()] || 0) { /* istanbul ignore next: can't test in JSDOM */ case +1: /* istanbul ignore next: can't test in JSDOM */ return `+50%p - ${arrowOffset}px` /* istanbul ignore next: can't test in JSDOM */ case -1: /* istanbul ignore next: can't test in JSDOM */ return `-50%p + ${arrowOffset}px` default: return 0 } } /* istanbul ignore next */ return this.offset }, popperCreate(el) { this.destroyPopper() // We use `el` rather than `this.$el` just in case the original // mountpoint root element type was changed by the template this.$_popper = new Popper(this.target, el, this.popperConfig) }, destroyPopper() { this.$_popper && this.$_popper.destroy() this.$_popper = null }, updatePopper() { this.$_popper && this.$_popper.scheduleUpdate() }, popperPlacementChange(data) { // Callback used by popper to adjust the arrow placement this.attachment = this.getAttachment(data.placement) }, /* istanbul ignore next */ renderTemplate(h) { // Will be overridden by templates return h('div') } }, render(h) { const { noFade } = this // Note: 'show' and 'fade' classes are only appled during transition return h( BVTransition, { // Transitions as soon as mounted props: { appear: true, noFade }, on: { // Events used by parent component/instance beforeEnter: el => this.$emit(EVENT_NAME_SHOW, el), afterEnter: el => this.$emit(EVENT_NAME_SHOWN, el), beforeLeave: el => this.$emit(EVENT_NAME_HIDE, el), afterLeave: el => this.$emit(EVENT_NAME_HIDDEN, el) } }, [this.localShow ? this.renderTemplate(h) : h()] ) } })