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

238 lines (232 loc) 6.67 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 Vue from '../../../utils/vue' import Popper from 'popper.js' import { getCS, select } from '../../../utils/dom' import { HTMLElement, SVGElement } from '../../../utils/safe-types' import { BVTransition } from '../../../utils/bv-transition' const NAME = 'BVPopper' 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 } // @vue/component export const BVPopper = /*#__PURE__*/ Vue.extend({ name: NAME, props: { target: { // Element that the tooltip/popover is positioned relative to type: [HTMLElement, SVGElement], default: null }, placement: { type: String, default: 'top' }, fallbackPlacement: { type: [String, Array], default: 'flip' }, offset: { type: Number, default: 0 }, boundary: { // 'scrollParent', 'viewport', 'window', or Element type: [String, HTMLElement], default: 'scrollParent' }, boundaryPadding: { // Tooltip/popover will try and stay away from // boundary edge by this many pixels type: Number, default: 5 }, arrowPadding: { // The minimum distance (in `px`) from the edge of the // tooltip/popover that the arrow can be positioned type: Number, default: 6 } }, data() { return { // reactive props set by parent noFade: false, // State related data localShow: true, attachment: this.getAttachment(this.placement) } }, computed: { templateType() /* istanbul ignore next */ { // Overridden by template component return 'unknown' }, popperConfig() { const placement = this.placement 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('show', el => { this.popperCreate(el) }) // Self destruct once hidden this.$on('hidden', () => { this.$nextTick(this.$destroy) }) // If parent is destroyed, ensure we are destroyed this.$parent.$once('hook:destroyed', this.$destroy) }, beforeMount() { // Ensure that the attachment position is correct before mounting // as our propsData is added after `new Template({...})` this.attachment = this.getAttachment(this.placement) }, mounted() { // TBD }, updated() { // Update popper if needed // TODO: Should this be a watcher on `this.popperConfig` instead? this.popperUpdate() }, beforeDestroy() { this.popperDestroy() }, 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 = (parseFloat(getCS(arrow).width) || 0) + (parseFloat(this.arrowPadding) || 0) switch (OffsetMap[String(placement).toUpperCase()] || 0) { case +1: /* istanbul ignore next: can't test in JSDOM */ return `+50%p - ${arrowOffset}px` 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.popperDestroy() // 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) }, popperDestroy() { this.$_popper && this.$_popper.destroy() this.$_popper = null }, popperUpdate() { this.$_popper && this.$_popper.scheduleUpdate() }, popperPlacementChange(data) { // Callback used by popper to adjust the arrow placement this.attachment = this.getAttachment(data.placement) }, renderTemplate(h) /* istanbul ignore next */ { // Will be overridden by templates return h('div') } }, render(h) { // Note: `show` and 'fade' classes are only appled during transition return h( BVTransition, { // Transitions as soon as mounted props: { appear: true, noFade: this.noFade }, on: { // Events used by parent component/instance beforeEnter: el => this.$emit('show', el), afterEnter: el => this.$emit('shown', el), beforeLeave: el => this.$emit('hide', el), afterLeave: el => this.$emit('hidden', el) } }, [this.localShow ? this.renderTemplate(h) : h()] ) } })