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
260 lines (246 loc) • 7.46 kB
JavaScript
// 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';
var NAME = 'BVPopper';
var 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'
};
var 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 var 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: function data() {
return {
// reactive props set by parent
noFade: false,
// State related data
localShow: true,
attachment: this.getAttachment(this.placement)
};
},
computed: {
templateType: function templateType()
/* istanbul ignore next */
{
// Overridden by template component
return 'unknown';
},
popperConfig: function popperConfig() {
var _this = this;
var 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: function onCreate(data) {
// Handle flipping arrow classes
if (data.originalPlacement !== data.placement) {
/* istanbul ignore next: can't test in JSDOM */
_this.popperPlacementChange(data);
}
},
onUpdate: function onUpdate(data) {
// Handle flipping arrow classes
_this.popperPlacementChange(data);
}
};
}
},
created: function created() {
var _this2 = this;
// 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', function (el) {
_this2.popperCreate(el);
}); // Self destruct once hidden
this.$on('hidden', function () {
_this2.$nextTick(_this2.$destroy);
}); // If parent is destroyed, ensure we are destroyed
this.$parent.$once('hook:destroyed', this.$destroy);
},
beforeMount: function 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: function mounted() {// TBD
},
updated: function updated() {
// Update popper if needed
// TODO: Should this be a watcher on `this.popperConfig` instead?
this.popperUpdate();
},
beforeDestroy: function beforeDestroy() {
this.popperDestroy();
},
destroyed: function destroyed() {
// Make sure template is removed from DOM
var el = this.$el;
el && el.parentNode && el.parentNode.removeChild(el);
},
methods: {
// "Public" method to trigger hide template
hide: function hide() {
this.localShow = false;
},
// Private
getAttachment: function getAttachment(placement) {
return AttachmentMap[String(placement).toUpperCase()] || 'auto';
},
getOffset: function getOffset(placement) {
if (!this.offset) {
// Could set a ref for the arrow element
var arrow = this.$refs.arrow || select('.arrow', this.$el);
var 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 - ".concat(arrowOffset, "px");
case -1:
/* istanbul ignore next: can't test in JSDOM */
return "-50%p + ".concat(arrowOffset, "px");
default:
return 0;
}
}
/* istanbul ignore next */
return this.offset;
},
popperCreate: function 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: function popperDestroy() {
this.$_popper && this.$_popper.destroy();
this.$_popper = null;
},
popperUpdate: function popperUpdate() {
this.$_popper && this.$_popper.scheduleUpdate();
},
popperPlacementChange: function popperPlacementChange(data) {
// Callback used by popper to adjust the arrow placement
this.attachment = this.getAttachment(data.placement);
},
renderTemplate: function renderTemplate(h)
/* istanbul ignore next */
{
// Will be overridden by templates
return h('div');
}
},
render: function render(h) {
var _this3 = this;
// 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: function beforeEnter(el) {
return _this3.$emit('show', el);
},
afterEnter: function afterEnter(el) {
return _this3.$emit('shown', el);
},
beforeLeave: function beforeLeave(el) {
return _this3.$emit('hide', el);
},
afterLeave: function afterLeave(el) {
return _this3.$emit('hidden', el);
}
}
}, [this.localShow ? this.renderTemplate(h) : h()]);
}
});