forked from DebaucheryLibrarian/traxxx
180 lines
3.1 KiB
Vue
180 lines
3.1 KiB
Vue
<template>
|
|
<div class="tooltip-container">
|
|
<div
|
|
ref="trigger"
|
|
class="trigger noselect"
|
|
@click.stop="toggle"
|
|
>
|
|
<slot />
|
|
</div>
|
|
|
|
<teleport to="body">
|
|
<div
|
|
v-if="opened"
|
|
ref="tooltip"
|
|
class="tooltip-wrapper"
|
|
:style="{ transform: `translate3d(${tooltipX}px, ${tooltipY}px, 0)` }"
|
|
@click.stop
|
|
>
|
|
<div class="tooltip-inner">
|
|
<div class="tooltip">
|
|
<slot name="tooltip" />
|
|
</div>
|
|
|
|
<div
|
|
class="tooltip-arrow"
|
|
:style="{ transform: `translate3d(${arrowOffset}px, 0, 0)` }"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { nextTick } from 'vue';
|
|
|
|
function getX(triggerBoundary, tooltipBoundary) {
|
|
const idealPosition = triggerBoundary.left + (triggerBoundary.width / 2) - (tooltipBoundary.width / 2);
|
|
const rightEdgeOverflow = Math.max((idealPosition + tooltipBoundary.width) - window.innerWidth, 0);
|
|
|
|
// don't overflow left edge
|
|
if (idealPosition < 0) {
|
|
return {
|
|
tooltipX: 0,
|
|
arrowOffset: idealPosition,
|
|
};
|
|
}
|
|
|
|
// don't overflow right edge
|
|
if (rightEdgeOverflow > 0) {
|
|
return {
|
|
tooltipX: window.innerWidth - tooltipBoundary.width,
|
|
arrowOffset: rightEdgeOverflow,
|
|
};
|
|
}
|
|
|
|
// position at the center of trigger
|
|
return {
|
|
tooltipX: idealPosition,
|
|
arrowOffset: 0,
|
|
};
|
|
}
|
|
|
|
async function calculate() {
|
|
if (!this.opened) {
|
|
return;
|
|
}
|
|
|
|
const triggerBoundary = this.$refs.trigger.getBoundingClientRect();
|
|
const tooltipBoundary = this.$refs.tooltip.getBoundingClientRect();
|
|
|
|
const { tooltipX, arrowOffset } = this.getX(triggerBoundary, tooltipBoundary);
|
|
|
|
this.tooltipY = triggerBoundary.top + triggerBoundary.height + 5;
|
|
this.tooltipX = tooltipX;
|
|
this.arrowOffset = arrowOffset;
|
|
}
|
|
|
|
async function open() {
|
|
this.events.emit('blur');
|
|
|
|
await nextTick();
|
|
|
|
this.opened = true;
|
|
await nextTick();
|
|
|
|
this.calculate();
|
|
this.$emit('open');
|
|
}
|
|
|
|
function close() {
|
|
this.opened = false;
|
|
|
|
this.tooltipY = 0;
|
|
this.tooltipX = 0;
|
|
this.arrowOffset = 0;
|
|
|
|
this.$emit('close');
|
|
}
|
|
|
|
function toggle() {
|
|
if (this.opened) {
|
|
this.close();
|
|
return;
|
|
}
|
|
|
|
this.open();
|
|
}
|
|
|
|
function mounted() {
|
|
this.events.on('blur', () => {
|
|
this.close();
|
|
});
|
|
|
|
this.events.on('resize', () => {
|
|
this.calculate();
|
|
});
|
|
|
|
this.events.on('scroll', () => {
|
|
this.calculate();
|
|
});
|
|
}
|
|
|
|
export default {
|
|
data() {
|
|
return {
|
|
opened: false,
|
|
tooltipX: 0,
|
|
tooltipY: 0,
|
|
arrowOffset: 0,
|
|
};
|
|
},
|
|
emits: ['open', 'close'],
|
|
mounted,
|
|
methods: {
|
|
calculate,
|
|
getX,
|
|
open,
|
|
close,
|
|
toggle,
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
.tooltip-wrapper {
|
|
display: flex;
|
|
top: 0;
|
|
left: 0;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
position: absolute;
|
|
z-index: 10;
|
|
}
|
|
|
|
.tooltip-inner {
|
|
position: relative;
|
|
box-shadow: 0 0 3px var(--darken-weak);
|
|
}
|
|
|
|
.tooltip {
|
|
position: relative;
|
|
background: var(--background);
|
|
}
|
|
|
|
.tooltip-arrow {
|
|
content: '';
|
|
width: 0;
|
|
height: 0;
|
|
position: absolute;
|
|
top: -.5rem;
|
|
left: calc(50% - .5rem);
|
|
border-left: .5rem solid transparent;
|
|
border-right: .5rem solid transparent;
|
|
border-bottom: .5rem solid var(--background);
|
|
margin: 0 auto;
|
|
filter: drop-shadow(0 0 3px var(--darken-weak));
|
|
}
|
|
</style>
|