<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)` }" > <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; 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(); }); } 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-light); } .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-light); margin: 0 auto; filter: drop-shadow(0 0 3px var(--darken-weak)); } </style>