audiobookshelf/client/components/ui/Tooltip.vue

167 lines
4.5 KiB
Vue

<template>
<div ref="box" @mouseover="mouseover" @mouseleave="mouseleave">
<slot />
</div>
</template>
<script>
export default {
props: {
text: {
type: [String, Number],
required: true
},
direction: {
type: String,
default: 'right'
},
/**
* Delay showing the tooltip after X milliseconds of hovering
*/
delayOnShow: {
type: Number,
default: 0
},
disabled: Boolean
},
data() {
return {
tooltip: null,
tooltipId: null,
isShowing: false,
hideTimeout: null,
delayOnShowTimeout: null
}
},
watch: {
text() {
this.updateText()
},
disabled(newVal) {
if (newVal && this.isShowing) {
this.hideTooltip()
}
}
},
methods: {
updateText() {
if (this.tooltip) {
this.tooltip.innerHTML = this.text
this.setTooltipPosition(this.tooltip)
}
},
createTooltip() {
if (!this.$refs.box) return
var tooltip = document.createElement('div')
this.tooltipId = String(Math.floor(Math.random() * 10000))
tooltip.id = this.tooltipId
tooltip.className = 'tooltip-wrapper absolute px-2 py-1 text-white text-xs rounded shadow-lg max-w-xs text-center hidden sm:block'
tooltip.style.zIndex = 100
tooltip.style.backgroundColor = 'rgba(0,0,0,0.85)'
tooltip.innerHTML = this.text
tooltip.addEventListener('mouseover', this.cancelHide)
tooltip.addEventListener('mouseleave', this.hideTooltip)
this.setTooltipPosition(tooltip)
this.tooltip = tooltip
},
setTooltipPosition(tooltip) {
const boxRect = this.$refs.box.getBoundingClientRect()
const shouldMount = !tooltip.isConnected
// Calculate size of tooltip
if (shouldMount) document.body.appendChild(tooltip)
const tooltipRect = tooltip.getBoundingClientRect()
if (shouldMount) tooltip.remove()
// Subtracting scrollbar size
const windowHeight = window.innerHeight - 8
const windowWidth = window.innerWidth - 8
let top = 0
let left = 0
if (this.direction === 'right') {
top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
left = Math.max(0, boxRect.left + boxRect.width + 4)
} else if (this.direction === 'bottom') {
top = Math.max(0, boxRect.top + boxRect.height + 4)
left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
} else if (this.direction === 'top') {
top = Math.max(0, boxRect.top - tooltipRect.height - 4)
left = Math.max(0, boxRect.left - tooltipRect.width / 2 + boxRect.width / 2)
} else if (this.direction === 'left') {
top = Math.max(0, boxRect.top - tooltipRect.height / 2 + boxRect.height / 2)
left = Math.max(0, boxRect.left - tooltipRect.width - 4)
}
// Shift left if tooltip would overflow the window on the right
if (left + tooltipRect.width > windowWidth) {
left -= left + tooltipRect.width - windowWidth
}
// Shift up if tooltip would overflow the window on the bottom
if (top + tooltipRect.height > windowHeight) {
top -= top + tooltipRect.height - windowHeight
}
tooltip.style.top = top + 'px'
tooltip.style.left = left + 'px'
},
showTooltip() {
if (this.disabled) return
if (!this.tooltip) {
this.createTooltip()
if (!this.tooltip) return
}
if (!this.$refs.box) return // Ensure element is not destroyed
try {
document.body.appendChild(this.tooltip)
this.setTooltipPosition(this.tooltip)
} catch (error) {
console.error(error)
}
this.isShowing = true
},
hideTooltip() {
if (!this.tooltip) return
this.tooltip.remove()
this.isShowing = false
},
cancelHide() {
clearTimeout(this.hideTimeout)
},
mouseover() {
if (this.isShowing || this.disabled) return
if (this.delayOnShow) {
if (this.delayOnShowTimeout) {
// Delay already running
return
}
this.delayOnShowTimeout = setTimeout(() => {
this.showTooltip()
this.delayOnShowTimeout = null
}, this.delayOnShow)
} else {
this.showTooltip()
}
},
mouseleave() {
if (!this.isShowing) {
clearTimeout(this.delayOnShowTimeout)
this.delayOnShowTimeout = null
return
}
this.hideTimeout = setTimeout(this.hideTooltip, 100)
}
},
beforeDestroy() {
this.hideTooltip()
}
}
</script>