上篇文章末尾,留了一个思考,就是当滑动表格到左右两端时改变mixinSwipeable的值,允许滑动到下一个或者上一个tab,下面会顺带讲到。
这次的主题是用原生操作dom的方式实现一个tooltip,类似echarts里面点击柱状图时tooltip的效果。
首先展示下效果
红圈部分是点击位置,tooltip跟着点击位置走,当点到屏幕中间偏左时,就设置left属性让它偏右,当点到屏幕中间偏右时,就设置right属性,让它偏左,另外要加一点外边距,避免溢出表格。当点到的单元格内容没有溢出时,就不显示(这个是一个关键点,后面有分析)。
下面来分析下
要实现这个效果,先看下vue-easytable这个库有没有自带,经过查看文档和审查元素后,发现有类似效果
接着审查下元素,发现就是最原始的,给标签添加title属性,属性值就是innerHtml值
显然,这种效果是不能满足要求的,所以只能自己实现一个了。那要做成什么样呢? pc端一般是hover时显示在上下左右方,那移动端不能hover,就只能是点击时显示,至于显示位置,有两种做法,第一种是像pc那样显示在上下左右方,第二种是像echarts柱状图那样,跟着点击位置显示。
第一种方案应该只能采用伪元素方式实现,可能要自定义渲染单元格了,会受到框架限制;第二种方案全部采用原生操作dom的方式,不受框架限制。
综合考虑后决定采用第二种方案。
核心步骤如下
- 监听body的
touchstart
事件,记录点击的节点类名
和坐标信息
- 节点类名用来判断是否点击到表格上,作为先决条件,然后判断是否溢出,这是第二个条件,只有两者都满足,才显示
- 坐标信息用来移动tooltip,保证跟着点击位置显示
- 通过改变
opacity
属性来控制显示隐藏,避免重排
详细步骤如下
首先跟着文档描述,先给每个表格一个固定宽度,然后添加单元格省略。
给完表格宽度后,就发现一个问题了,原本固定的列头不能固定了,因为滚动内容撑满了外层容器。审查元素后,发现确实如此。
这是主要的几个节点
这是设置500px的固定宽度后,三个节点的实际宽度,显然,500px加在了最外层的容器上了,子元素再一层层往上继承,看上去就是滚动内容撑满了外层容器,没有了滚动条,固定列自然就失效了
要解决这个问题,就是让500px直接作用于滚动内容,也就是table元素上,所以就需要样式穿透了,直接在.ve-table上添加一个自定义的类,然后用/deep/
穿透
.my-table {
/deep/.ve-table-content {
width: 500px;
}
}
然后再审查下元素,就生效了
解决了固定列,这只是第一步,接下来就是给几个列添加单元格溢出属性了
然后又发现了一个问题,除了中文,纯字母纯数字都无法显示省略号,这又是为啥?
经过一番了解后,发现只要添加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'
});
加上后看下效果:
这个问题解决了,总可以开始回归正题,实现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的全过程,中途踩了不少坑,痛并快乐着。痛是因为自己之前没有记录开发过程中碰到问题的习惯,也没有总结的习惯,有点不适应;快乐是因为实现了需求,巩固加强了一些之前模棱两可的知识。
最后
文笔有限,如果有没有表述清楚的,还请多多包涵,有错误的地方,万望告知,有什么疑问和建议,可以多多交流。