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.
367 lines (325 loc) • 11 kB
JavaScript
function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; var ownKeys = Object.keys(source); if (typeof Object.getOwnPropertySymbols === 'function') { ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) { return Object.getOwnPropertyDescriptor(source, sym).enumerable; })); } ownKeys.forEach(function (key) { _defineProperty(target, key, source[key]); }); } return target; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
/*
* 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 ---
var 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'
};
var 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: function data() {
return {
// semaphore for preventing multiple show events
localShow: false
};
},
computed: {
baseConfig: function baseConfig() {
var cont = this.container;
var 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 : "#".concat(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: function show(_show, old) {
if (_show !== old) {
_show ? this.onOpen() : this.onClose();
}
},
disabled: function disabled(_disabled, old) {
if (_disabled !== old) {
_disabled ? this.onDisable() : this.onEnable();
}
},
localShow: function localShow(show, old) {
if (show !== this.show) {
this.$emit('update:show', show);
}
}
},
created: function created() {
// Create non-reactive property
this._toolpop = null;
this._obs_title = null;
this._obs_content = null;
},
mounted: function mounted() {
var _this = this;
// We do this in a next tick to ensure DOM has rendered first
this.$nextTick(function () {
// 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: function updated() {
// If content/props changes, etc
if (this._toolpop) {
this._toolpop.updateConfig(this.getConfig());
}
},
activated: function 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: function 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: function 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: function getConfig() {
var cfg = _objectSpread({}, 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: function onOpen() {
if (this._toolpop && !this.localShow) {
this.localShow = true;
this._toolpop.show();
}
},
onClose: function 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: function onDisable() {
if (this._toolpop) {
this._toolpop.disable();
}
},
onEnable: function onEnable() {
if (this._toolpop) {
this._toolpop.enable();
}
},
updatePosition: function 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: function getTarget() {
var 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: function onShow(evt) {
this.$emit('show', evt);
this.localShow = !(evt && evt.defaultPrevented);
},
onShown: function onShown(evt) {
this.setObservers(true);
this.$emit('shown', evt);
this.localShow = true;
},
onHide: function onHide(evt) {
this.$emit('hide', evt);
this.localShow = !!(evt && evt.defaultPrevented);
},
onHidden: function 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: function 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: function 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: function 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: function 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;
}
}
}
}
};