v-overflowTooltip解决el-table的show-overflow-tooltip不能复制问题

1,693 阅读3分钟

写在前面

二次封装el-table时看到了show-overflow-tooltip,当内容过长被隐藏时显示 tooltip。感觉体验挺好就加上了这个属性。之后发现tooltip里的文字是复制不了的,鼠标离开单元格后tooltip瞬间就关闭了。复制表格内容的场景还是很常见的,所以这个问题也是必须要解决的了。

先去文档查看,发现组件不支持后去element的issues搜索了下,发现很多人都遇到了这个问题。也有前辈提供解决方案。用el-tooltip代替show-overflow-tooltip,但是效果度不太满意,未溢出的文字也会有tooltip,而且每个单元格都渲染了el-tooltip,性能上也有影响。想了下决定自己来实现了。


分析需求

我想要的达到效果

  • 文字超出单元格不换行,并出现省略号
  • 鼠标移入出现tooltip,并且能移入tooltip进行复制
  • 文字没有溢出的不需要出现tooltip
  • 代码可复用,复用时比较简单

确定实现方案

由于改的是el-table的功能,所以先去看了下el-table的源码,看到在单元格移入事件里执行了createTablePopper方法。很明显createTablePopper就是我们要看的核心代码。

根据路径找到utils.ts文件下的createTablePopper,在方法里看到el-table是用的popperjs渲染弹框,useZIndex动态计算弹框元素的z-index样式属性。并且在元素的鼠标移出时执行removePopper删除弹框元素。

看完源码后可以大致确定为以下方案

  • 用CSS实现内容溢出出现省略号
  • 在元素上绑定鼠标移入事件,使用popperjs插件,动态渲染tooltip
  • 根据元素的scrollWidthoffsetWidth判断内容是否溢出(是否需要出现popper
  • 弹框元素销毁时加上300ms延迟,并且移入弹框元素后取消销毁,实现能移入弹框复制文字
  • 使用vue的自定义指令封装,达到代码可以轻松复用

进行编码

在自定义指令的created钩子里绑定css样式,实现内容溢出出现省略号

export default {
  name: 'overflowTooltip',
  directive: {
    created (el) {
      el.style.overflow = 'hidden'
      el.style.textOverflow = 'ellipsis'
      el.style.whiteSpace = 'nowrap'
    }
  }
}

判断元素是否需要popper。注册移入事件,渲染popper

import { createPopper } from '@popperjs/core'
import { useZIndex } from 'element-plus/es/hooks/use-z-index/index'

function renderArrow () {
  const arrow = document.createElement('div')
  arrow.className = 'el-popper__arrow'
  return arrow
}
function renderContent (value) {
  const { nextZIndex } = useZIndex()
  const content = document.createElement('div')
  content.className = 'el-popper is-dark'
  content.innerHTML = value
  content.style.zIndex = String(nextZIndex())
  document.body.appendChild(content)
  return content
}
export default {
  name: 'overflowTooltip',
  directive: {
    created (el) {
      el.style.overflow = 'hidden'
      el.style.textOverflow = 'ellipsis'
      el.style.whiteSpace = 'nowrap'
    },
    mounted (el, binding) {
      // 判断元素是否需要popper
      if (el.scrollWidth <= el.offsetWidth) {
        return
      }
      let removePopperTime = null

      el.addEventListener('mouseover', () => {
        // Popper显示的内容
        const value = binding.value || el.textContent
        if (!value) {
          return
        }
        // 创建Popper元素
        const content = renderContent(value)
        // 创建Popper小三角
        const arrow = renderArrow()
        content.appendChild(arrow)

        // 调用插件,渲染Popper
        createPopper(el, content, {
          strategy: 'absolute',
          placement: 'top',
          modifiers: [
            {
              name: 'offset',
              options: {
                offset: [0, 8]
              }
            },
            {
              name: 'arrow',
              options: {
                element: arrow,
                padding: 10
              }
            }
          ]
        })
    }
  }
}

绑定元素移出事件,实现删除逻辑

import { createPopper } from '@popperjs/core'
import { useZIndex } from 'element-plus/es/hooks/use-z-index/index'

function renderArrow () {
  const arrow = document.createElement('div')
  arrow.className = 'el-popper__arrow'
  return arrow
}
function renderContent (value) {
  const { nextZIndex } = useZIndex()
  const content = document.createElement('div')
  content.className = 'el-popper is-dark'
  content.innerHTML = value
  content.style.zIndex = String(nextZIndex())
  document.body.appendChild(content)
  return content
}
export default {
  name: 'overflowTooltip',
  directive: {
    created (el) {
      el.style.overflow = 'hidden'
      el.style.textOverflow = 'ellipsis'
      el.style.whiteSpace = 'nowrap'
    },
    mounted (el, binding) {
      if (el.scrollWidth <= el.offsetWidth) {
        return
      }
      let removePopperTime = null

      el.addEventListener('mouseover', () => {
        // 判断当前元素有没有未删除的Popper,有则阻止删除并且return
        if (removePopperTime) {
          clearTimeout(removePopperTime)
          removePopperTime = null
          return
        }
        // Popper显示的内容
        const value = binding.value || el.textContent
        if (!value) {
          return
        }
        // 创建Popper元素
        const content = renderContent(value)
        // 创建Popper小三角
        const arrow = renderArrow()
        content.appendChild(arrow)

        // 调用插件,渲染Popper
        createPopper(el, content, {
          strategy: 'absolute',
          placement: 'top',
          modifiers: [
            {
              name: 'offset',
              options: {
                offset: [0, 8]
              }
            },
            {
              name: 'arrow',
              options: {
                element: arrow,
                padding: 10
              }
            }
          ]
        })
        // 删除
        const removePopper = () => {
          removePopperTime = setTimeout(() => {
            try {
              content && document.body.removeChild(content)
              el.removeEventListener('mouseout', removePopper)
            } catch {}
            clearTimeout(removePopperTime)
            removePopperTime = null
          }, 300)
        }
        // 移入弹框后取消删除
        content.addEventListener('mouseover', () => {
          clearTimeout(removePopperTime)
          removePopperTime = null
        })
        // 元素移出
        el.addEventListener('mouseout', removePopper)
        // 弹框移出
        content.addEventListener('mouseout', removePopper)
      })
    }
  }
}

到这里这个指令也就完成了,以上就是指令的完整代码。

使用

在二次封装的table里使用。封装成指令后也可以在所有元素上使用。