用原生操作Dom实现tooltip? 安排!

1,332 阅读6分钟

上篇文章末尾,留了一个思考,就是当滑动表格到左右两端时改变mixinSwipeable的值,允许滑动到下一个或者上一个tab,下面会顺带讲到。

这次的主题是用原生操作dom的方式实现一个tooltip,类似echarts里面点击柱状图时tooltip的效果。

首先展示下效果

点击效果.png

红圈部分是点击位置,tooltip跟着点击位置走,当点到屏幕中间偏左时,就设置left属性让它偏右,当点到屏幕中间偏右时,就设置right属性,让它偏左,另外要加一点外边距,避免溢出表格。当点到的单元格内容没有溢出时,就不显示(这个是一个关键点,后面有分析)。

下面来分析下

要实现这个效果,先看下vue-easytable这个库有没有自带,经过查看文档和审查元素后,发现有类似效果

默认的hover效果.png

接着审查下元素,发现就是最原始的,给标签添加title属性,属性值就是innerHtml值

设置title属性.png

显然,这种效果是不能满足要求的,所以只能自己实现一个了。那要做成什么样呢? pc端一般是hover时显示在上下左右方,那移动端不能hover,就只能是点击时显示,至于显示位置,有两种做法,第一种是像pc那样显示在上下左右方,第二种是像echarts柱状图那样,跟着点击位置显示。

第一种方案应该只能采用伪元素方式实现,可能要自定义渲染单元格了,会受到框架限制;第二种方案全部采用原生操作dom的方式,不受框架限制。

综合考虑后决定采用第二种方案。

核心步骤如下

  • 监听body的touchstart事件,记录点击的节点类名坐标信息
    • 节点类名用来判断是否点击到表格上,作为先决条件,然后判断是否溢出,这是第二个条件,只有两者都满足,才显示
    • 坐标信息用来移动tooltip,保证跟着点击位置显示
  • 通过改变opacity属性来控制显示隐藏,避免重排

详细步骤如下

首先跟着文档描述,先给每个表格一个固定宽度,然后添加单元格省略。

table外层容器宽度.png

给完表格宽度后,就发现一个问题了,原本固定的列头不能固定了,因为滚动内容撑满了外层容器。审查元素后,发现确实如此。

这是主要的几个节点

table容器节点信息.png

这是设置500px的固定宽度后,三个节点的实际宽度,显然,500px加在了最外层的容器上了,子元素再一层层往上继承,看上去就是滚动内容撑满了外层容器,没有了滚动条,固定列自然就失效了

滚动宽度问题.png

要解决这个问题,就是让500px直接作用于滚动内容,也就是table元素上,所以就需要样式穿透了,直接在.ve-table上添加一个自定义的类,然后用/deep/穿透

.my-table {
  /deep/.ve-table-content {
    width: 500px;
  }
}

然后再审查下元素,就生效了

滚动宽度问题1.png

解决了固定列,这只是第一步,接下来就是给几个列添加单元格溢出属性了

ve-table单元格省略属性.png

然后又发现了一个问题,除了中文,纯字母纯数字都无法显示省略号,这又是为啥?

非中文没有打点效果.png

经过一番了解后,发现只要添加word-break: break-all就可以了,字面理解word-break就是破坏文本结构,break-all就是破坏所有类型的文本,比如字母数字中文等等。

仔细看完文档和审查元素后确定,这是框架目前没有实现的,所以又要操作dom了,在每个包含表格的页面mounted里添加下面的代码

const ellipsisCellList = document.getElementsByClassName('ve-table-body-td-span-ellipsis')
ellipsisCellList.forEach(ele => {
  ele.style.wordBreak = 'break-all'
});

加上后看下效果:

word-break.png

这个问题解决了,总可以开始回归正题,实现tooltip了吧?没错!

第一步:拿到body元素,添加touchstart事件

mounted() {
    const myBody = document.getElementsByTagName('body')[0]
    myBody.addEventListener('touchstart', this.mixinTabPageHandler, false)
},

第二步:执行滑动tab逻辑和tooltip逻辑

mixinTabPageHandler(event) {
    const touchDomClassName = event.target.className
    this.mixinSwipeableHandler(touchDomClassName)
    this.mixinTableTooltipHandler(event, touchDomClassName)
}
mixinSwipeableHandler(touchDomClassName) {
    // 表格的节点类名前缀都是ve-table
    // 所以只要点击的节点类名不包含ve-table,就说明没点到表格,就允许滑动tab
    this.mixinSwipeable = !touchDomClassName.includes('ve-table')
},
mixinTableTooltipHandler(event, touchDomClassName) {
  // 拿到点击的坐标信息 
  const pageX = event.touches[0].pageX
  const pageY = event.touches[0].pageY
  // table的外边距
  const tableMargin = 10

  // 如果没有创建tooltip,就创建一个
  if (this.tooltipDom === undefined) {
    this.tooltipDom = document.createElement('div')
    // 给一个类名
    this.tooltipDom.className = 'tooltip'
    this.tooltipDom.style.position = 'fixed'
    this.tooltipDom.style.backgroundColor = 'rgba(32, 33, 36,.7)'
    this.tooltipDom.style.borderColor = 'rgba(32, 33, 36,0.20)'
    this.tooltipDom.style.borderWidth = '1px'
    this.tooltipDom.style.borderRadius = '4px'
    this.tooltipDom.style.color = '#fff'
    this.tooltipDom.style.zIndex = 11
    this.tooltipDom.style.padding = '2px 5px 4px'
    this.tooltipDom.style.fontSize = '12px'
    // 分别给一个最小宽度和最大宽度  
    this.tooltipDom.style.minWidth = '20px'
    this.tooltipDom.style.maxWidth = '50vw'
    // 默认设置不可见,等进一步判断
    this.tooltipDom.style.opacity = 0
  }

  const myBody = document.getElementsByTagName('body')[0]
  // 如果body最后一个节点的类名不是tooltip,说明是第一次挂载,就可以挂载
  if (myBody.lastChild.className !== 'tooltip') {
    myBody.appendChild(this.tooltipDom)
  }      

  // 挂载好之后,就可以开始填充节点内容,改变tooltip位置了
  // 因为是用的固定定位,不会引起重排,只需要改变top,left,right等位置属性就可以了
  this.tooltipDom.innerHTML = event.target.innerHTML
  this.tooltipDom.style.top = pageY + 'px'

  // 获取屏幕的宽度
  const screenWidth = window.innerWidth
  if (pageX < screenWidth / 2) {
    // 如果点击的位置在中间偏左
    // 重置right
    this.tooltipDom.style.right = 'auto'  
    // 修改left
    this.tooltipDom.style.left = pageX + 'px'
    // 加点右外边距避免溢出
    this.tooltipDom.style.marginRight = tableMargin + 'px'
  } else {
    // 如果点击的位置在中间偏右,同理
    this.tooltipDom.style.left = 'auto'
    this.tooltipDom.style.right = screenWidth - pageX + 'px'
  }
  
  /**
   * 到了最关键的一步了,如何判断单元格内容是否溢出?
   * 只有溢出了,点击后才会显示,否则隐藏
   */
   
  // 上面说到,添加了单元格省略的几个属性后,就会在td里的span标签上添加ve-table-body-td-span-ellipsis这个类
  // 所以可以先做第一步判断,判断点到的单元格是否能够溢出省略
  if (touchDomClassName !== 've-table-body-td-span-ellipsis') {
    this.tooltipDom.style.opacity = 0
  } else {
    // 然后再判断是否溢出
    // 先获取单元格宽度
    const cellSpanWidth = event.target.clientWidth
    if (this.tooltipDom.clientWidth > cellSpanWidth) {
      // 如果tooltip宽度大于单元格宽度,说明超出了,显示
      // 这里有个细节,因为上面创建tooltip时是用的opacity,节点是存在的,所以有实际宽度
      this.tooltipDom.style.opacity = 1
    } else {
      this.tooltipDom.style.opacity = 0
    }
  }
  
  // 通过给表格容器添加滚动事件,监听表格是否滚动,一旦有滚动,就隐藏
  const tableContainer = document.getElementsByClassName('ve-table-container')[0]
  // 如果页面包含表格,才需要执行tooltip逻辑,因为tooltip只是表格需要用到
  if (!tableContainer) return
  tableContainer.addEventListener('scroll', (e) => {
    // 计算滚动的距离
    const scrollLeft = e.target.scrollLeft
    if (scrollLeft > 0) {
      // 只要滚动了,就隐藏
      this.tooltipDom.style.opacity = 0
    } else {
      // 说明没有滚动或者滚回到最左侧
      console.log('可以滑到上一个tab了')
      this.mixinSwipeable = true
    }
    
    /*
    再判断是否滚动到最右侧,判断是否能滑到下一个tab
    只要滚动内容的宽度 = 容器的宽度 + 滚动的距离,就说明滚到最右侧了
    */
    
    // 计算滚动内容的宽度
    const scrollWidth = document.getElementsByClassName('ve-table-content')[0].scrollWidth
    if (scrollWidth === tableContainer.clientWidth + scrollLeft) {
      console.log('可以滑到下一个tab了')
      this.mixinSwipeable = true
    }
  })
},

总结一下

本文记录了自己采用原生操作dom实现tooltip的全过程,中途踩了不少坑,痛并快乐着。痛是因为自己之前没有记录开发过程中碰到问题的习惯,也没有总结的习惯,有点不适应;快乐是因为实现了需求,巩固加强了一些之前模棱两可的知识。

最后

文笔有限,如果有没有表述清楚的,还请多多包涵,有错误的地方,万望告知,有什么疑问和建议,可以多多交流。