blob: 9634c42603adf04b52c1d0ab5ca6510d9be07b2a [file] [log] [blame]
/** * Licensed to the Apache Software Foundation (ASF) under one or more *
contributor license agreements. See the NOTICE file distributed with * this work
for additional information regarding copyright ownership. * The ASF licenses
this file to You under the Apache License, Version 2.0 * (the "License"); you
may not use this file except in compliance with * the License. You may obtain a
copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless
required by applicable law or agreed to in writing, software * distributed under
the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR
CONDITIONS OF ANY KIND, either express or implied. * See the License for the
specific language governing permissions and * limitations under the License. */
<template>
<component :is="tagName">
<slot name="reference"></slot>
<span ref="popper" v-show="!disabled && showPopper">
<div class="popper">
<slot></slot>
</div>
</span>
</component>
</template>
<script>
import Popper from 'popper.js';
const on = (element, event, handler) => {
if (element && event && handler) {
document.addEventListener
? element.addEventListener(event, handler, false)
: element.attachEvent('on' + event, handler);
}
};
const off = (element, event, handler) => {
if (element && event) {
document.removeEventListener
? element.removeEventListener(event, handler, false)
: element.detachEvent('on' + event, handler);
}
};
export default {
props: {
tagName: {
type: String,
default: 'span',
},
trigger: {
type: String,
default: 'hover',
validator: (value) =>
[
'clickToOpen',
'click', // Same as clickToToggle, provided for backwards compatibility.
'clickToToggle',
'hover',
'focus',
].indexOf(value) > -1,
},
delayOnMouseOver: {
type: Number,
default: 10,
},
delayOnMouseOut: {
type: Number,
default: 10,
},
disabled: {
type: Boolean,
default: false,
},
boundariesSelector: String,
reference: {},
forceShow: {
type: Boolean,
default: false,
},
dataValue: {
default: null,
},
appendToBody: {
type: Boolean,
default: false,
},
stopPropagation: {
type: Boolean,
default: false,
},
preventDefault: {
type: Boolean,
default: false,
},
options: {
type: Object,
default() {
return {};
},
},
},
data() {
return {
referenceElm: null,
popperJS: null,
showPopper: false,
currentPlacement: '',
popperOptions: {
placement: 'bottom-start',
computeStyle: {
gpuAcceleration: false,
},
},
};
},
watch: {
showPopper(value) {
if (value) {
this.$emit('show', this);
if (this.popperJS) {
this.popperJS.enableEventListeners();
}
this.updatePopper();
} else {
if (this.popperJS) {
this.popperJS.disableEventListeners();
}
this.$emit('hide', this);
}
},
forceShow: {
handler(value) {
this[value ? 'doShow' : 'doClose']();
},
immediate: true,
},
disabled(value) {
if (value) {
this.showPopper = false;
}
},
},
created() {
this.appendedArrow = false;
this.appendedToBody = false;
this.popperOptions = Object.assign(this.popperOptions, this.options);
},
mounted() {
this.referenceElm = this.reference || this.$slots.reference[0].elm;
this.popper = this.$slots.default[0].elm;
switch (this.trigger) {
case 'clickToOpen':
on(this.referenceElm, 'click', this.doShow);
on(document, 'click', this.handleDocumentClick);
break;
case 'click': // Same as clickToToggle, provided for backwards compatibility.
case 'clickToToggle':
on(this.referenceElm, 'click', this.doToggle);
on(document, 'click', this.handleDocumentClick);
break;
case 'hover':
on(this.referenceElm, 'mouseover', this.onMouseOver);
on(this.popper, 'mouseover', this.onMouseOver);
on(this.referenceElm, 'mouseout', this.onMouseOut);
on(this.popper, 'mouseout', this.onMouseOut);
break;
case 'focus':
on(this.referenceElm, 'focus', this.onMouseOver);
on(this.popper, 'focus', this.onMouseOver);
on(this.referenceElm, 'blur', this.onMouseOut);
on(this.popper, 'blur', this.onMouseOut);
break;
}
},
methods: {
doToggle(event) {
if (this.stopPropagation) {
event.stopPropagation();
}
if (this.preventDefault) {
event.preventDefault();
}
if (!this.forceShow) {
this.showPopper = !this.showPopper;
}
},
doShow() {
this.showPopper = true;
},
doClose() {
this.showPopper = false;
},
doDestroy() {
if (this.showPopper) {
return;
}
if (this.popperJS) {
this.popperJS.destroy();
this.popperJS = null;
}
if (this.appendedToBody) {
this.appendedToBody = false;
document.body.removeChild(this.popper.parentElement);
}
},
createPopper() {
this.$nextTick(() => {
if (this.appendToBody && !this.appendedToBody) {
this.appendedToBody = true;
document.body.appendChild(this.popper.parentElement);
}
if (this.popperJS && this.popperJS.destroy) {
this.popperJS.destroy();
}
if (this.boundariesSelector) {
const boundariesElement = document.querySelector(
this.boundariesSelector,
);
if (boundariesElement) {
this.popperOptions.modifiers = Object.assign(
{},
this.popperOptions.modifiers,
);
this.popperOptions.modifiers.preventOverflow = Object.assign(
{},
this.popperOptions.modifiers.preventOverflow,
);
this.popperOptions.modifiers.preventOverflow.boundariesElement = boundariesElement;
}
}
this.popperOptions.onCreate = () => {
this.$emit('created', this);
this.$nextTick(this.updatePopper);
};
this.popperJS = new Popper(
this.referenceElm,
this.popper,
this.popperOptions,
);
});
},
destroyPopper() {
off(this.referenceElm, 'click', this.doToggle);
off(this.referenceElm, 'mouseup', this.doClose);
off(this.referenceElm, 'mousedown', this.doShow);
off(this.referenceElm, 'focus', this.doShow);
off(this.referenceElm, 'blur', this.doClose);
off(this.referenceElm, 'mouseout', this.onMouseOut);
off(this.referenceElm, 'mouseover', this.onMouseOver);
off(document, 'click', this.handleDocumentClick);
this.showPopper = false;
this.doDestroy();
},
updatePopper() {
this.popperJS ? this.popperJS.scheduleUpdate() : this.createPopper();
},
onMouseOver() {
clearTimeout(this._timer);
this._timer = setTimeout(() => {
this.showPopper = true;
}, this.delayOnMouseOver);
},
onMouseOut() {
clearTimeout(this._timer);
this._timer = setTimeout(() => {
this.showPopper = false;
}, this.delayOnMouseOut);
},
handleDocumentClick(e) {
if (
!this.$el ||
!this.referenceElm ||
this.elementContains(this.$el, e.target) ||
this.elementContains(this.referenceElm, e.target) ||
!this.popper ||
this.elementContains(this.popper, e.target)
) {
return;
}
this.$emit('documentClick', this);
if (this.forceShow) {
return;
}
this.showPopper = false;
},
elementContains(elm, otherElm) {
if (typeof elm.contains === 'function') {
return elm.contains(otherElm);
}
return false;
},
},
destroyed() {
this.destroyPopper();
},
};
</script>
<style lang="scss">
.popper {
z-index: 11;
}
.popper .popper__arrow {
width: 0;
height: 0;
border-style: solid;
position: absolute;
margin: 5px;
}
.popper[x-placement^='top'] {
margin-bottom: 5px;
}
.popper[x-placement^='top'] .popper__arrow {
border-width: 5px 5px 0 5px;
border-color: #fafafa transparent transparent transparent;
bottom: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.popper[x-placement^='bottom'] {
margin-top: 5px;
}
.popper[x-placement^='bottom'] .popper__arrow {
border-width: 0 5px 5px 5px;
border-color: transparent transparent #fafafa transparent;
top: -5px;
left: calc(50% - 5px);
margin-top: 0;
margin-bottom: 0;
}
.popper[x-placement^='right'] {
margin-left: 5px;
}
.popper[x-placement^='right'] .popper__arrow {
border-width: 5px 5px 5px 0;
border-color: transparent #fafafa transparent transparent;
left: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
.popper[x-placement^='left'] {
margin-right: 5px;
}
.popper[x-placement^='left'] .popper__arrow {
border-width: 5px 0 5px 5px;
border-color: transparent transparent transparent #fafafa;
right: -5px;
top: calc(50% - 5px);
margin-left: 0;
margin-right: 0;
}
</style>