最新的vue3文字气泡提示Tooltip已发布github,有问题和需求欢迎大佬来GitHub提issue,欢迎star和fork,谢谢!!!!
来吧展示
在我们项目开发中,经常会有超长文本溢出提示,未溢出则不提示的场景。
在项目开发中遇到了比较复杂的场景,在一个组织树中,我们使用了el-tree来显示组织树,文字长度不一,太长的显示不全,刚开始我们使用滚动条,结果不好看
后来我们就直接再el-tree中添加el-tooltip,发现没啥问题鼠标移入正常提示,但是再往下滚动的时候发现,页面会卡顿,因为每个el-tree都会产生一个el-tooltip,
只不过是通过v-if进行显示和隐藏的,频繁显示性能开销很大,性能极差,页面中还会出现一堆的 注释
想着开发一个指令,与el-tooltip类似,超出显示…移入显示内容
如下图所示,
<script lang="ts" setup>
import {ref} from "vue";
const treeData = ref([
{
"id": "65118224801137459281926519380023",
"title": "测试",
"level": 1,
"parentId": "464064658652315648",
"type": "PROJECT",
"enable": 1,
"list": [
{
"id": "65141629137928601691056522250367",
"title": "未超出长度",
"level": 2,
"parentId": "65118224801137459281926519380023",
"type": "PROJECT",
"enable": 1,
"list": []
},
{
"id": "65141629137928601691056522250368",
"title": "超出隐藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏",
"level": 2,
"parentId": "65118224801137459281926519380023",
"type": "PROJECT",
"enable": 1,
"list": []
},
{
"id": "65141629137928601691056522250369",
"title": "超出隐藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏",
"level": 2,
"parentId": "65118224801137459281926519380023",
"type": "PROJECT",
"enable": 1,
"list": []
}
]
},
])
const defaultProps = {
children: 'list',
label: 'title',
}
const tooltipTitle = ref("")
const showTitle = ref(true)
const onShowNameTipsMouseenter = (e) => {
let target = e.target;
let textLength = target.clientWidth;
let containerLength = target.scrollWidth;
if (textLength < containerLength) {
tooltipTitle.value = e.target.innerText;
showTitle.value = false;
} else {
showTitle.value = true;
}
}
</script>
<template>
<el-tree
:data="treeData"
:props="defaultProps"
style="width: 150px"
>
<template #default="{ node }">
<el-tooltip
:content="tooltipTitle"
:disabled="showTitle"
effect="dark"
placement="top"
>
<span
class="span-ellipsis"
@mouseover="onShowNameTipsMouseenter"
>{{ node.label }}</span
>
</el-tooltip>
</template>
</el-tree>
</template>
<style lang="scss" scoped>
.span-ellipsis {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 20px;
}
</style>
这样的代码比较复杂,而且复用性极低。如果在其他页面也有类似的场景,我们就不得不cv了
指令开发
如何判断是否溢出
这也算是一个知识点,首先我们需要判断文本是否溢出了节点。如何解决文本是否溢出,在element-ui中el-table有一个文本溢出可以隐藏,所以我再其源码中找到了这个element-ui/packages/table/src/table-body.js,判断溢出的方法,代码如下
const range = document.createRange();
range.setStart(cellChild, 0);
range.setEnd(cellChild, cellChild.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) +
(parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0);
if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
// ...
}
我们稍微改造一下,拿来用
实现溢出指令
function getElStyleAttr(element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
mounted(el, binding) {
// 避免用户遗漏样式,我们必须强制加上超出...样式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseEnter = (e) => {
if (isOverflow(el)) {
console.log('溢出了')
} else {
console.log('未溢出')
}
}
el.addEventListener('mouseenter', onMouseEnter)
}
}
看下效果吧
如何把溢出的节点挂载到el-tooltip上
我们不直接像el-tooltip挂载到每个文本上面,这样就和el-tooltip没啥区别
我们直接把它添加在全局body下,而且每次直会显示一个,移出就隐藏删除添加的tooltip
首先我们需要准备好组件
MyTooltip.vue
<template>
<!-- 指示 -->
<transition name="el-fade-in-linear">
<div v-show="tooltipShow" :style="tooltipStyle" class="wq-tooltip"
>
<span class="wq-tooltip-text" v-text="text"></span>
<div :class="[
{'left':placements === 'left'},
{'bottom':placements==='bottom'},
{'right':placements==='right'},
{'top':placements==='top'}]" class="wq-tooltip-arrow"></div>
</div>
</transition>
</template>
<script>
import {ref, computed, onMounted} from 'vue'
export default {
setup() {
// 显示弹框
const tooltipShow = ref(false);
// 提示内容
const text = ref()
// 方向
const placements = ref('left')
// 显示
function showTip() {
tooltipShow.value = true
}
//设置提示内容
function setContent(content) {
text.value = content
}
//隐藏
function hiddenTip() {
tooltipShow.value = false
}
// 位置
const tooltipPosition = ref({
x: 0,
y: 0
})
const tooltipStyle = computed(() => {
return {
transform: `translate3d(${tooltipPosition.value.x}px,${tooltipPosition.value.y}px,0)`
}
})
return {
tooltipShow,
showTip,
hiddenTip,
setContent,
tooltipPosition,
tooltipStyle,
text,
placements,
}
}
}
</script>
<style lang="scss" scoped>
// tooltip
.wq-tooltip {
padding: 10px;
font-size: 12px;
line-height: 1.2;
min-width: 10px;
word-wrap: break-word;
position: fixed;
left: 0;
top: 0;
background: #303133;
color: #fff;
z-index: 1000;
display: block;
border-radius: 8px;
font-weight: 500;
pointer-events: none;
}
// 小箭头
.wq-tooltip-arrow {
position: absolute;
width: 0;
height: 0;
border-width: 8px;
border-style: solid;
}
// 如果在左侧
.wq-tooltip-arrow.left {
border-color: transparent transparent transparent #303133;
right: -15px;
top: 50%;
transform: translate3d(0, -50%, 0);
}
// 如果在下侧
.wq-tooltip-arrow.bottom {
top: -15px;
border-color: transparent transparent #303133 transparent;
left: 50%;
transform: translate3d(-50%, 0, 0);
}
// 如果在右侧
.wq-tooltip-arrow.right {
left: -15px;
top: 50%;
transform: translate3d(0, -50%, 0);
border-color: transparent #303133 transparent transparent;
}
// 如果在上侧
.wq-tooltip-arrow.top {
bottom: -15px;
border-color: #303133 transparent transparent transparent;
left: 50%;
transform: translate3d(-50%, 0, 0);
}
/* 动画 */
.tooltip-enter-from,
.tooltip-leave-to {
opacity: 0;
transition: opacity .3s ease;
}
.tooltip-leave-from,
.tooltip-enter-to {
transition: opacity .1s ease;
}
</style>
在指令js中鼠标移入时挂载该组件,移出时销毁组件
directive.js
// 引入组件
import {createApp, nextTick} from "vue";
import MyToolTip from './MyToolTip.vue'
// 位置定位
function calculationLocation(el, target, placements) {
if (!el || !target) return;
el.tooltipPosition.y = 0;
el.tooltipPosition.x = 0;
let el_dom = el.$el.nextElementSibling.getBoundingClientRect()
let target_dom = target.getBoundingClientRect()
if (placements === "left") {
el.tooltipPosition.x = target_dom.x - el_dom.width - 10
el.tooltipPosition.y = target_dom.y - el_dom.height / 2 + target_dom.height / 2
} else if (placements === "bottom") {
el.tooltipPosition.x = target_dom.x + target_dom.width / 2 - el_dom.width / 2
el.tooltipPosition.y = target_dom.y + el_dom.height + 10
} else if (placements === "right") {
el.tooltipPosition.x = target_dom.x + target_dom.width + 10
el.tooltipPosition.y = target_dom.y - el_dom.height / 2 + target_dom.height / 2
} else if (placements === "top") {
el.tooltipPosition.x = target_dom.x + target_dom.width / 2 - el_dom.width / 2
el.tooltipPosition.y = target_dom.y - el_dom.height - 10
}
}
// 方向
const allPlacements = ['left', 'bottom', 'right', 'top']
function getElStyleAttr(element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
mounted(el, binding) {
//获取指令的参数
const {
value: {
placement, content, destroyOnLeave
} = {}
} = binding;
// 加上超出...样式
el.style.overflow = "hidden";
el.style.textOverflow = "ellipsis";
el.style.whiteSpace = "nowrap";
//鼠标移开时 清除元素
const onMouseLeave = () => {
if (el.w_tipInstance) {
el.w_tipInstance.hiddenTip()
el.w_tooltip.remove()
el.w_tipInstance = null
el.w_tooltip = null
}
};
const onMouseEnter = () => {
// 判断内容长度 需要展示
if (isOverflow(el)) {
const directiveList = allPlacements.filter(placement => binding.modifiers[placement])
const placements = directiveList.length ? directiveList : allPlacements
if (!el.w_tooltip) {
// 创建tooltip实例
const vm = createApp(MyToolTip)
// 创建根元素
el.w_tooltip = document.createElement('div')
// 挂载到页面
document.body.appendChild(el.w_tooltip)
el.w_tooltip.id = `tooltip_${Math.floor(Math.random() * 10000)}`
el.w_tipInstance = vm.mount(el.w_tooltip)
}
// 设置 tooltip 显示方向
el.w_tipInstance.placements = placement || placements[0] || 'top'
// 设置显示内容
el.w_tipInstance.setContent(content || el.innerText)
// 使 tooltip 显示
el.w_tipInstance.showTip()
nextTick(() => {
// 计算 tooltip 在页面中的位置
calculationLocation(el.w_tipInstance, el, placements[0])
})
el._scrollHandler = () => {
// 重新定位位置
if (el.w_tipInstance && el.w_tipInstance.tooltipShow) calculationLocation(el.w_tipInstance, el, placements[0])
}
window.addEventListener('scroll', el._scrollHandler)
const _destroyOnLeave = destroyOnLeave || true
if (_destroyOnLeave) el.addEventListener("mouseleave", onMouseLeave);
}
};
el.addEventListener("mouseenter", onMouseEnter);
},
unmounted(el) {
if (el.w_tooltip) {
document.body.removeChild(el.w_tooltip)
}
window.removeEventListener('scroll', el._scrollHandler)
}
}
指令和组件都好了,我们就在全局配置指令即可
import App from './App.vue'
import {ellipsisTooltip} from './directive.js'
const app = createApp(App);
app.directive('ellipse-tooltip', ellipsisTooltip);
app.mount('#app');
配置好全局指令后,页面中就可以直接使用了
<script lang="ts" setup>
import {ref} from "vue";
const treeData = ref([
{
"id": "65118224801137459281926519380023",
"title": "测试",
"level": 1,
"parentId": "464064658652315648",
"type": "PROJECT",
"enable": 1,
"list": [
{
"id": "65141629137928601691056522250367",
"title": "未超出长度",
"level": 2,
"parentId": "65118224801137459281926519380023",
"type": "PROJECT",
"enable": 1,
"list": []
},
{
"id": "65141629137928601691056522250368",
"title": "超出隐藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏",
"level": 2,
"parentId": "65118224801137459281926519380023",
"type": "PROJECT",
"enable": 1,
"list": []
},
{
"id": "65141629137928601691056522250369",
"title": "超出隐藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏藏",
"level": 2,
"parentId": "65118224801137459281926519380023",
"type": "PROJECT",
"enable": 1,
"list": []
}
]
},
])
const defaultProps = {
children: 'list',
label: 'title',
}
const tooltipTitle = ref("")
const showTitle = ref(true)
const onShowNameTipsMouseenter = (e) => {
let target = e.target;
let textLength = target.clientWidth;
let containerLength = target.scrollWidth;
if (textLength < containerLength) {
tooltipTitle.value = e.target.innerText;
showTitle.value = false;
} else {
showTitle.value = true;
}
}
</script>
<template>
<el-tree
:data="treeData"
:props="defaultProps"
style="width: 150px"
>
<template #default="{ node }">
<!-- <el-tooltip-->
<!-- :content="tooltipTitle"-->
<!-- :disabled="showTitle"-->
<!-- effect="dark"-->
<!-- placement="top"-->
<!-- >-->
<span v-ellipse-tooltip.top
class="span-ellipsis"
@mouseover="onShowNameTipsMouseenter"
>{{ node.label }}</span
>
<!-- </el-tooltip>-->
</template>
</el-tree>
</template>
<style lang="scss" scoped>
.span-ellipsis {
width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 20px;
}
</style>
看看效果
到这里我们的ellipse-tooltip指令就完成了,还有很大改进的地方,需要大佬们自行去完善了