如何实现一个tooltip指令

1,560 阅读3分钟

导读

最近碰到一个问题,一个标题过长导致了换行,页面效果类似于下面这样

我就想用css的的ellipsis来让超出最大允许宽度的后显示省略号,但是呢,鼠标浮动还是要显示标题的是吧?加上目前项目又是使用了element-ui,那就用tooltip组件就完事了。然而,标题很短很短(不知道有没有某根牙签短)的时,我并不想让它显示tooltip,这就需要在mouseenter时做出判断了。然鹅,本来想套tooltip发现实在有点达不到预想的效果,又研究了tooltip的底层实现机制,发现了popperjs这个库,就很完美了!

本文基于vue2

知识导读

要充分理解以下内容,你需要了解以下知识点:

实现步骤

css 样式设置

首先毫无疑问地,我们的目标必须将文字内容超出给定的宽度显示省略号,通常css将这样设置

.tooltip{
  width: 700px; /*超出此宽度后显示省略号*/
  text-overflow: ellipsis;
  overflow: hidden;
  white-space:nowrap;
}

在给目标设置css样式时,我们可以通过el拿到目标dom, 然后设置style(內联写法),我们将写成这样的代码

el.style.setproperty("width","700px");

感觉有点心智负担,如果看过element的部分实现,可以在src/utils/dom.js文件下找到一些实用的方法,比如setStyle

setStyle方法

//el即是指令绑定的元素
setStyle(el, 'text-overflow', 'ellipsis')

创建popper

首先引入库包

npm i @popperjs/core -S

然后直接引入创建方法

import {createPopper} from "@popperjs/core"

使用popper必须制定目标元素和作为tooltip的元素 tooltip其实也应该是一个div,通过研究Element-UItooltip可知,其div是插入到body下方的,因此我们的代码如下

const div =document.createElement('div');
div.textContent=content;
addClass(div, 'auto-tooltip')
div.setAttribute('id','tooltip')
div.setAttribute('role','tooltip')
const body = document.querySelector('body')
body.appendChild(div)
createPopper(el,div,{
    placement:placement
})

宽度判断

这里是自动功能的核心,鼠标浮动时我们必须知道目标元素中文字的长度是否超过了指定宽度,是否需要tooltip的功能。 以下是获取文本在真实dom内部的代码 MDN参考

const range=document.createRange();
range.setStart(el,0);
range.setEnd(el,el.childNodes.length);
const rangeWidth = Math.round(range.getBoundingClientRect().width);

通过与预设的长度width比较我们就能知道我们是需要tooltip的了。

tooltip样式设定

这里的样式设定几乎都照抄Element-UItooltip样式,没啥好说的。

逻辑优化

功能做完了,我们就要重新梳理,得到完整的代码了,相关的说明直接放代码注释里,请看下图:

Vue.directive('auto-tooltip', {
  inserted: function(el, binding, vNode) {
    //get value from binding.value;
    // console.log(el.childNodes.length);
    //tooltip的placement属性, 阈值宽度和主题均可以通过绑定得到,防止为undefined,
    //预设初始值
    var placement = binding?.value?.placement ?? 'bottom';
    var width=binding?.value?.width ?? 700;
    var effect=binding?.value?.effect ?? "dark"
    //ellipsis省略样式
    setStyle(el, 'max-width',width+'px')
    setStyle(el, 'text-overflow', 'ellipsis')
    setStyle(el, 'overflow', 'hidden')
    setStyle(el, 'display', 'block')
    setStyle(el, 'white-space', 'nowrap')
    //mouseenter事件
    el.addEventListener('mouseenter', function() {
      //没有ellipsis时的真实宽度
      const range=document.createRange();
      range.setStart(el,0);
      range.setEnd(el,el.childNodes.length);
      const rangeWidth = Math.round(range.getBoundingClientRect().width);
      //与阈值宽度做比较
      if(rangeWidth>width){
        createTooltip();
      }
    })
    //鼠标离开的mouseleave事件中移除所有的tooltip dom
    el.addEventListener('mouseleave', function(e) {
      const divs = document.querySelectorAll('.auto-tooltip')
      divs.forEach(function(div) {
        div.remove()
      })
    })
    //创建tooltip的逻辑单独封装
    function createTooltip(){
      var content=el.innerText; //设置内容
      var div = document.createElement('div')
      div.textContent=content;
      //样式设定
      addClass(div, 'auto-tooltip')
      div.setAttribute('id','tooltip')
      div.setAttribute('role','tooltip')
      setStyle(div,'position','absolute')
      setStyle(div,'border-radius','4px')
      setStyle(div,'padding','10px');
      setStyle(div,'z-index','2000');
      setStyle(div,'font-size','14px');
      setStyle(div,'line-height',1.2);
      setStyle(div,'min-width','12px');
      setStyle(div,'word-wrap','break-word');
      setStyle(div,'background',effect=='dark'?'#303133':'#FFF');
      if(effect=='dark'){
        setStyle(div,'color','#303133');
        setStyle(div,'border','solid 1px #303133');
      }
      setStyle(div,'color','#FFF');
      const body = document.querySelector('body')
      body.appendChild(div)
      //使用popperjs创建
      createPopper(el,div,{
        placement:placement
      })
    }
    // el.addEventListener('mouseenter',addToolTip);
  }

总结&回顾

通过Popperjs我们实现了类似于Element-UI中的tooltip功能,并通过指令的形式全局注册。同时,使用了不常见的Range.getBoundingClientRect()方法来识别我们的文字区域(因为文字部分也将作为一个TextNode成为DOM的一部分)的宽度来判断我们是否需要创建tooltip。