traxxx/assets/components/tooltip/tooltip.vue

185 lines
3.2 KiB
Vue
Executable File

<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"
:style="{ 'max-height': `calc(100vh - ${tooltipY}px - 1rem)` }"
>
<div class="tooltip">
<slot name="tooltip" />
</div>
</div>
<div
class="tooltip-arrow"
:style="{ transform: `translate3d(${arrowOffset}px, 0, 0)` }"
/>
</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 {
emits: ['open', 'close'],
data() {
return {
opened: false,
tooltipX: 0,
tooltipY: 0,
arrowOffset: 0,
};
},
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;
overflow-y: auto;
background: var(--background);
box-shadow: 0 0 .5rem var(--darken);
}
.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>