The Tooltip component is usually used to interpret elements on a page, and I think it is usually used to quickly interpret objects that can be summarized in a nutshell, which is lightweight.

There is a UI component library

First of all, I would like to pay tribute to my predecessors, who have provided a lot of inspiration for my component packaging. 🖖🖖🖖(salute)

However, some mainstream UI libraries, such as ElementUI and Bootstrap, require component packages for Tooltip components, which I find troublesome, so TODAY I use vue3.x custom instructions to package a simple Tooltip component.

Effect and usage

As shown in figure,TooltipYes support similari18nThis may update parameters dynamically, and is much easier to use:

<template>
    <! Create tooltip tooltip tooltip tooltip tooltip tooltip tooltip
    <button v-tooltip.left="t('like')">give a like<button>
</template>

<script>
import { useI18n } from 'vue-i18n'
export default {
    setup(){
        const { t } = useI18n();
        
        return {
            t
        }
    }
}
</script>

<! The i18N single file mode used here is not explained here, if you have any questions can be discussed in the comment section.
<i18n>{" zh_CN ": {" like" : "three even a key"}, "en_US" : {" like ":" like me "}}</i18n>

Copy the code

Other positions

<button v-tooltip.left="' Displayed on the left '">give a like<button>
<button v-tooltip.right="' Displayed on the right '">give a like<button>
<button v-tooltip.top="' Shown on top '">give a like<button>
<button v-tooltip.bottom="' Shown below '">give a like<button>
Copy the code

Directory format

Tooltip | | - directive. Js / / registered here custom instructions & positioning tooltip | | - tooltip. Vue / / write tooltip template hereCopy the code

tooltip.vue

There is nothing to say here, but define templates that expose properties that can be used to control positions.

<template>
  <! - instructions -- -- >
  <div class="zc-tooltip" v-show="tooltipShow" :style="tooltipStyle">
      <! -- Directive content -->
      <span class="zc-tooltip-text" v-html="text"></span>
      <! -- Small arrow -->
      <div class="zc-tooltip-arrow" :class="[{'left':placements=='left'}, {'bottom':placements=='bottom'}, {'right':placements=='right'}, {'top':placements=='top'}]"></div>
  </div>
</template>

<script>
import {ref, computed} from 'vue'
export default {
    setup(){
        // Display the popbox
        const tooltipShow = ref(false);

        // Prompt content
        const text = ref()

        // Display direction
        const placements = ref('left')
        
        / / show
        function showTip(){
            tooltipShow.value = true
        }
        / / hide
        function hiddenTip(){
            tooltipShow.value = false
        }

        / / position
        const tooltipPostiton = ref({
            x: 0.y: 0
        })
        const tooltipStyle = computed(() = >{
          return {
            transform: `translate3d(${tooltipPostiton.value.x}px,${tooltipPostiton.value.y}px,0)`}})return {
            tooltipShow,
            showTip,
            hiddenTip,
            tooltipPostiton,
            tooltipStyle,
            text,
            placements,
        }
    }
}
</script>

Copy the code

🌟🌟🌟 Solve the problem that if the mouse moves over the edge of the Tooltip, 🌟 port 🌟 may flash

The reason for the flash is simple:

Graph TB A[mouse over button] --> B[display Tooltip] --> C{mouse over button edge} C== yes ==>D Hover over the Tooltip] - > [button loses focus] -- > G F [Tooltip disappear] = = = = > infinite loop A C = = n = = > E [normal display]

The most common way to prevent jitter is to give a delay of about 150ms, but TODAY I am more simple and violent. CSS has a property pointers-events: Specifies under what circumstances (if any) a particular element can become a mouse event. If set to None, the browser ignores mouse events for the current element.

/* causes the element to ignore mouse events */
.zc-tooltip{...pointer-events: none;
}
Copy the code

directive.js

Directive.js mainly registers directives. Note the difference in component registration between vue3 and vue2.

export default {
    install(app) {
        // The lifecycle hooks here are different from vue2
        app.directive('tooltip', {
            mounted(el, binding){... },updated(el, binding){... },unmounted(el){... }}}}Copy the code

mounted()

In Mounted we mainly:

  1. createTooltipInstance and mount to the page.
  2. Receive the prompt content and direction from the command.
  3. Evaluates based on the bound elementTooltipPosition on the page.
// Control the direction
const allPlacements = ['left'.'bottom'.'right'.'top']...mounted(el, binding) {
    // Get the prompt content
    el._tipOptions = binding.value
    // When the mouse moves over the target element
    el._tipHandler = () = > {
        Get the correct display direction from the command modifiers
        const limitPlacementQueue = allPlacements.filter(placement= > binding.modifiers[placement])
        const placements = limitPlacementQueue.length ? limitPlacementQueue : allPlacements
        
        // If no instance exists
        if(! el._tipInstance) {// Create a tooltip instance
            el._synopsis = createApp(tooltip)
            // Create root element
            el._root = document.createElement('div')
            // Mount to the page
            document.body.appendChild(el._root)
            el._tipInstance = el._synopsis.mount(el._root)
        }
        // Set the orientation of the tooltip display
        el._tipInstance.placements = placements[0]
        // Make the tooltip display
        el._tipInstance.showTip()
        // Set the tooltip display
        el._tipInstance.text = el._tipOptions
        nextTick(() = > {
            // Calculate the position of the tooltip on the page
            calculationLocation(el._tipInstance, el, placements[0])})// When a scroll event is sent
        el._scrollHandler = () = > {
            if (el._tipInstance.tooltipShow)
                // Reposition the position
                calculationLocation(el._tipInstance, el, placements[0])}// Add page scroll listener
        window.addEventListener('scroll', el._scrollHandler)
        }
        
        // Mouse over the target element
        el._tipMouseleaveHandler = () = > {
            if (el._tipInstance) {
                // Make tooltip hidden
                el._tipInstance.hiddenTip()
             }
        }
        
        // Add mouse listener to the target element
        el.addEventListener('mouseenter', el._tipHandler)
        el.addEventListener('mouseleave', el._tipMouseleaveHandler)
    },
Copy the code

updated()

In the updated, we mainly update the prompt content to cope with possible parameter changes.

updated(el, binding) {
    // Update the prompt
    el._tipOptions = binding.value
},
Copy the code

unmounted()

Unmounted we mainly:

  1. uninstallTooltipInstance.
  2. Remove each listening event.
unmounted(el) {
    if (el._tipInstance) {
        // Uninstall the Tooltip instance.
        el._synopsis.unmount()
        document.body.removeChild(el._root)
    }
    // Remove all listener events
    window.removeEventListener('scroll', el._scrollHandler)
}
Copy the code

The source code to show

tooltip.vue

<template>
  <! - instructions -- -- >
  <transition name="tooltip">
    <div class="zc-tooltip" v-show="tooltipShow" :style="tooltipStyle" 
          >
      <span class="zc-tooltip-text" v-html="text"></span>
      <div class="zc-tooltip-arrow" :class="[{'left':placements=='left'}, {'bottom':placements=='bottom'}, {'right':placements=='right'}, {'top':placements=='top'}]"></div>
    </div>
  </transition>
</template>

<script>
import {ref, computed} from 'vue'
export default {
    setup(){

        // Display the popbox
        const tooltipShow = ref(false);

        // Prompt content
        const text = ref()

        / / direction
        const placements = ref('left')
        
        / / show
        function showTip(){
            tooltipShow.value = true
        }
        function hiddenTip(){
            tooltipShow.value = false
        }

        / / position
        const tooltipPostiton = ref({
            x: 0.y: 0
        })
        const tooltipStyle = computed(() = >{
          return {
            transform: `translate3d(${tooltipPostiton.value.x}px,${tooltipPostiton.value.y}px,0)`}})return {
            tooltipShow,
            showTip,
            hiddenTip,
            tooltipPostiton,
            tooltipStyle,
            text,
            placements,
        }
    }
}
</script>

<style lang="scss" scoped>
// tooltip
.zc-tooltip{
  padding: 10px;
  font-size: 12px;
  line-height: 1.2;
  min-width: 10px;
  word-wrap: break-word;
  position: fixed;
  left: 0;
  top: 0;  
  background: # 303133;
  color: #fff;
  z-index: 1000;
  display: inline-block;
  border-radius: 8px;
  font-weight: 500;
  pointer-events: none; } // small arrow.zc-tooltip-arrow{
  position: absolute;
  width: 0;
  height: 0;
  border-width: 8px;
  border-style: solid; } // If on the left.zc-tooltip-arrow.left{
  border-color: transparent transparent transparent # 303133;
  right: -15px;
  top: 50%;
  transform: translate3d(0, -50%.0); } // If on the lower side.zc-tooltip-arrow.bottom{
  top: -15px;
  border-color: transparent transparent # 303133 transparent;
  left: 50%;
  transform: translate3d(-50%.0.0); } // If it is on the right.zc-tooltip-arrow.right{
  left: -15px;
  top: 50%;
  transform: translate3d(0, -50%.0);
  border-color: transparent # 303133transparent transparent; } // If on the top side.zc-tooltip-arrow.top{
  bottom: -15px;
  border-color: # 303133 transparent transparent transparent;
  left: 50%;
  transform: translate3d(-50%.0.0);
}

/ * * / animation
.tooltip-enter-from..tooltip-leave-to{
  opacity: 0;
  transition: opacity .3s ease;
}
.tooltip-leave-from..tooltip-enter-to{
  transition: opacity .1s ease;
}
</style>
Copy the code

directive.js

// Import components
import { nextTick, createApp } from "vue";
import tooltip from './tooltip.vue'
import { tokenFun } from '.. /.. /utils/token'


// Clear the listener
function clearEvent(el) {
    if (el._tipHandler) {
        el.removeEventListener('mouseenter', el._tipHandler)
    }
    if (el._tipMouseleaveHandler) {
        el.removeEventListener('mouseleave', el._tipMouseleaveHandler)
    }
    delete el._tipHandler
    delete el._tipMouseleaveHandler
    delete el._tipOptions
    delete el._tipInstance
}

// The location is fixed
function calculationLocation(el, target, placements) {
    if(! el || ! target)return;
    el.tooltipPostiton.y = 0;
    el.tooltipPostiton.x = 0;
    let el_dom = el.$el.nextElementSibling.getBoundingClientRect()
    let target_dom = target.getBoundingClientRect()

    if (placements === "left") {
        el.tooltipPostiton.x = target_dom.x - el_dom.width - 10
        el.tooltipPostiton.y = target_dom.y - el_dom.height / 2 + target_dom.height / 2
    } else if (placements === "bottom") {
        el.tooltipPostiton.x = target_dom.x + target_dom.width / 2 - el_dom.width / 2
        el.tooltipPostiton.y = target_dom.y + el_dom.height + 10
    } else if (placements === "right") {
        el.tooltipPostiton.x = target_dom.x + target_dom.width + 10
        el.tooltipPostiton.y = target_dom.y - el_dom.height / 2 + target_dom.height / 2
    } else if (placements === "top") {
        el.tooltipPostiton.x = target_dom.x + target_dom.width / 2 - el_dom.width / 2
        el.tooltipPostiton.y = target_dom.y - el_dom.height - 10}}/ / direction
const allPlacements = ['left'.'bottom'.'right'.'top']

export default {
    install(app) {
        app.directive('tooltip', {
            mounted(el, binding) {
                clearEvent(el)
                el._tipOptions = binding.value
                el._tipHandler = () = > {
                    const limitPlacementQueue = allPlacements.filter(placement= > binding.modifiers[placement])
                    const placements = limitPlacementQueue.length ? limitPlacementQueue : allPlacements
                    if(! el._tipInstance) { el._synopsis = createApp(tooltip) el._root =document.createElement('div')
                        document.body.appendChild(el._root)
                        el._root.id = `tooltip_${tokenFun()}`
                        el._tipInstance = el._synopsis.mount(el._root)
                    }
                    el._tipInstance.placements = placements[0]
                    el._tipInstance.showTip()
                    el._tipInstance.text = el._tipOptions
                    nextTick(() = > {
                        calculationLocation(el._tipInstance, el, placements[0])
                    })
                    el._scrollHandler = () = > {
                        if (el._tipInstance.tooltipShow)
                            calculationLocation(el._tipInstance, el, placements[0])}window.addEventListener('scroll', el._scrollHandler)
                }
                el._tipMouseleaveHandler = () = > {
                    if (el._tipInstance) {
                        el._tipInstance.hiddenTip()
                    }
                }
                el.addEventListener('mouseenter', el._tipHandler)
                el.addEventListener('mouseleave', el._tipMouseleaveHandler)
            },
            updated(el, binding) {
                el._tipOptions = binding.value
            },
            unmounted(el) {
                if (el._tipInstance) {
                    el._synopsis.unmount()
                    document.body.removeChild(el._root)
                }
                window.removeEventListener('scroll', el._scrollHandler)
            }
        })
    }
}
Copy the code

token.js

Used to assign a unique ID to each tooltip and can be ignored

function node() {
    return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
}

export function tokenFun() {
    return (node() + node() + node());
}
Copy the code

main.js

import { createApp } from 'vue'
import App from './App.vue'

/ / install the tooltip
import tooltip from './components/tooltip/directive'

const app = createApp(App)
app.use(tooltip).mount('#app')
Copy the code

Quick to use

<button v-tooltip.left="' Displayed on the left '">O praise<button>
<button v-tooltip.right="' Displayed on the right '">O praise<button>
<button v-tooltip.top="' Shown on top '">O praise<button>
<button v-tooltip.bottom="' Shown below '">O praise<button>
Copy the code

Urls you might use

i18n

Vue3 instruction lifecycle hook

Places to expand

v-tooltip.left=”Obejct”: The tooltip component in this article has a lot to add. For example, you can pass in data of type Obejct to configure more projects by checking whether the type of binding.value is a string or an object in directive.js.

The Tooltip component designed in this article creates a unique display of tooltip elements after the first hover over the target element, and the next hover simply updates the existing tooltip for maximum performance.

Of course, you can change billions of bits of code so that the page displays only one tooltip at a time.

Your likes are my biggest motivation, and if you have any better suggestions for tooltip component optimization, feel free to comment in the comments section.