Skip to content

自定义Vue指令,使用 element-plus 实现文本溢出后鼠标悬浮显示tooltip的功能

需求

文本超出盒子宽度后显示省略号,鼠标悬浮到该盒子上时,使用ElTooptip组件显示文本的全部内容,没有超出的文本鼠标悬浮时不显示ElTooptip

效果图

实现思路

  1. 使用CSS设置文本超出后显示省略号,代码如下:

    css
    .box {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
  2. 自定义Vue指令,指令的工作流程如下:

    • 给绑定指令的DOM元素添加mouseenter事件。
    • 事件触发后实时计算文本占用的原始宽度和显示的宽度进行比较。
    • 如果原始占用的宽度大于显示的宽度,则表示内容被隐藏掉了。
    • 使用VuehcreateVNode函数动态创建ElTooptip,然后使用render函数进行渲染就可以了。

代码实现

计算文本占用宽度,判断是否需要创建ElTootip组件。

typescript
const range = document.createRange()
range.setStart(el, 0)
range.setEnd(el, el.childNodes.length)

let rangeWidth = range.getBoundingClientRect().width
const offsetWidth = rangeWidth - Math.floor(rangeWidth)
const { width: cellChildWidth } = el.getBoundingClientRect()

if (offsetWidth < 0.001) {
	rangeWidth = Math.floor(rangeWidth)
}
if (rangeWidth > cellChildWidth) {
    // 创建Tooltip组件
	createTooltip(el.innerText, el, binding.value || {})
}

创建ElTooltip组件。

typescript
let removePopper: RemovePopperFn | null = null

const createTooltip = (content: string, trigger: HTMLElement, props: TextOverflowProp) => {
  if (removePopper?.trigger === trigger) {
    return
  }
  removePopper?.()

  const vm = createVNode(ElTooltip, {
    content: content,
    virtualTriggering: true,
    virtualRef: trigger,
    hideAfter: 0,
    ...props,
    onHide: () => {
      removePopper?.()
    }
  })
  const container = document.createElement('div')
  render(vm, container)
  vm.component!.exposed!.onOpen()
  const scrollContainer = document.querySelector('.el-scrollbar__wrap')
  removePopper = () => {
    render(null, container)
    scrollContainer?.removeEventListener('scroll', removePopper!)
    removePopper = null
  }
  removePopper.trigger = trigger
  scrollContainer?.addEventListener('scroll', removePopper)
}

text-overflow/index.ts完整代码如下:

typescript
import { ElTooltip } from 'element-plus'
import { createVNode, render } from 'vue'
import type { CSSProperties, Directive, DirectiveBinding, VNode } from 'vue'

type RemovePopperFn = (() => void) & {
  trigger?: HTMLElement
}

export interface TextOverflowProp {
  placement?:
    | 'top'
    | 'top-start'
    | 'top-end'
    | 'bottom'
    | 'bottom-start'
    | 'bottom-end'
    | 'left'
    | 'left-start'
    | 'left-end'
    | 'right'
    | 'right-start'
    | 'right-end'
  content?: string
  offset?: number
}

let removePopper: RemovePopperFn | null = null

const createTooltip = (content: string, trigger: HTMLElement, props: TextOverflowProp) => {
  if (removePopper?.trigger === trigger) {
    return
  }
  removePopper?.()

  const vm = createVNode(ElTooltip, {
    content: content,
    virtualTriggering: true,
    virtualRef: trigger,
    hideAfter: 0,
    ...props,
    onHide: () => {
      removePopper?.()
    }
  })

  const container = document.createElement('div')
  render(vm, container)
  vm.component!.exposed!.onOpen()
  const scrollContainer = document.querySelector('.el-scrollbar__wrap')
  removePopper = () => {
    render(null, container)
    scrollContainer?.removeEventListener('scroll', removePopper!)
    removePopper = null
  }
  removePopper.trigger = trigger
  scrollContainer?.addEventListener('scroll', removePopper)
}

/**
 * 文本超出显示范围后鼠标放到文本上显示tooltip
 */
const textOverflowDirective = {
  mounted: (el: HTMLElement, binding: DirectiveBinding<TextOverflowProp>) => {
    el.onmouseenter = () => {
      if (el.childNodes.length === 0) {
        return
      }

      const range = document.createRange()
      range.setStart(el, 0)
      range.setEnd(el, el.childNodes.length)
      let rangeWidth = range.getBoundingClientRect().width
      const offsetWidth = rangeWidth - Math.floor(rangeWidth)
      const { width: cellChildWidth } = el.getBoundingClientRect()
      if (offsetWidth < 0.001) {
        rangeWidth = Math.floor(rangeWidth)
      }
      if (rangeWidth > cellChildWidth) {
        createTooltip(el.innerText, el, binding.value || {})
      }
    }
  }
} as Directive<HTMLElement, TextOverflowProp>

export const vTextOverflow = textOverflowDirective
export default vTextOverflow

使用示例

vue
<script setup lang="ts">
import vTextOverflow from '@/components/directives/text-overflow'
</script>

<template>
  <div class="box">
    <ul>
      <li v-text-overflow>这是第一条这是第一条这是第一条这是第一条</li>
      <li v-text-overflow>这是第二条</li>
      <li v-text-overflow>这是第三条</li>
      <li v-text-overflow>这是第四条这是第四条这是第四条这是第四条</li>
    </ul>
  </div>  
</template>

<style scoped lang="scss">
.box {
  width: 200px;
  height: 300px;
  border: 2px solid #eee;
  margin:  40px 100px;
  padding: 10px;
}

ul {
  margin: 0;
  padding: 0;
  list-style: none;
  line-height: 36px;

  li {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
}
</style>

ElementPlus Playground