一、需求背景:
tooltip是前端开发中一个常见的功能,当文案内容超出一行会要求一行显示末尾显示省略号,在其右边显示一个info图标,hover这个info图标显示完整文案的tooltip,本文就是简单写一个指令实现这个效果
二、框架版本
Vue3版本:
"vue": "^3.0.0"
Element Plus版本:
"element-plus": "^2.3.12"
三、参考资料地址:
四、实现过程:
1.MyTooltip组件
写一个MyTooltip组件,使用elment-plus或者vant之类或者其他框架,总之props只有label
TypeScript
<template>
<el-tooltip
effect="dark"
:content="label"
>
<van-icon name="info-o" class="info-o-icon" @click="handleInfo"></van-icon>
</el-tooltip>
</template>
<script lang="ts" setup>
import { toRefs } from 'vue';
const props = defineProps<{
label: string;
}>();
const { label } = toRefs(props);
const handleInfo = (e: Event) => {
e.stopPropagation();
};
</script>
<style lang="scss" scoped>
.info-o-icon {
position: absolute;
top: 10px;
right: -15px;
width: 20px;
height: 20px;
z-index: 2000;
}
.info-o-text {
white-space: pre-wrap;
}
</style>
大家可以把van-icon换成element-plus框架的图标,保持框架的一致性,比如:
TypeScript
<template>
<el-tooltip
effect="dark"
:content="label"
>
<InfoFilled name="info-o" class="info-o-icon" @click="handleInfo" />
</el-tooltip>
</template>
<script lang="ts" setup>
import { toRefs } from 'vue';
import { InfoFilled } from '@element-plus/icons-vue'
const props = defineProps<{
label: string;
}>();
const { label } = toRefs(props);
const handleInfo = (e: Event) => {
e.stopPropagation();
};
</script>
<style lang="scss" scoped>
.info-o-icon {
position: absolute;
top: 10px;
right: -15px;
width: 20px;
height: 20px;
z-index: 2000;
}
.info-o-text {
white-space: pre-wrap;
}
</style>
2.自定义指令完整代码
TypeScript
import { Directive, h, render, DirectiveBinding } from 'vue';
import MyTooltip from '../components/common/MyTooltip.vue';
//测量子节点长度(必须在mounted之后)
export function range(el: HTMLElement) {
const range = document.createRange();
range.setStart(el, 0);
range.setEnd(el, el.childNodes.length);
return range.getBoundingClientRect().width;
}
//是否超长(必须在mounted之后)
export function isOverflow(el: HTMLElement) {
const dom = el.querySelector('.xxx'); // 找到内容文案的dom
console.log(`clientWidth: ${dom?.clientWidth}, scrollWidth: ${dom?.scrollWidth}`);
if (dom) {
return dom.scrollWidth > dom.clientWidth;
}
const targetW = el.getBoundingClientRect().width;
console.log('targetW:', targetW, 'range(el):', range(el), range(el) > targetW);
return range(el) > targetW;
}
/**
*
* @param el
* @param bind
* 使用示例:
* <div v-ellipsis-tooltip>
* <div class="text-ellipsis">这是一段很长的文字。。。。。。。。。。。。</div>
<div class="v-ellipsis-tooltip__container"></div>
</div>
*/
export function overflowTooltip(el: HTMLElement, bind: DirectiveBinding) {
el.style.position = 'relative';
const firstDom = el.children[0] as HTMLElement;
firstDom.style.overflow = 'hidden';
firstDom.style.textOverflow = 'ellipsis';
firstDom.style.whiteSpace = 'nowrap';
const triggerDom = el.children[el.children.length - 1] as HTMLElement;
if (!isOverflow(firstDom)) {
triggerDom.classList.contains('v-ellipsis-tooltip__container') && render(null, triggerDom);
} else {
const text = firstDom.innerText;
const tooltip = h(MyTooltip, { label: bind.value || text || '' });
triggerDom.classList.contains('v-ellipsis-tooltip__container') &&
render(tooltip, triggerDom as HTMLElement);
// 对tooltip的trigger进行修改样式
// const child = triggerDom.children[0] as HTMLElement;
// const style = child.style;
// style.background = 'red';
}
}
// 自定义指令
export const vEllipsisTooltip: Directive = {
created() {
console.log('------created-------');
},
beforeMount() {
console.log('------beforeMount-------');
},
mounted(el: HTMLElement, dir: DirectiveBinding) {
console.log('------mounted-------');
overflowTooltip(el, dir);
},
beforeUpdate(el, dir) {
console.log('------beforeUpdate-------', el, dir);
},
updated(el: HTMLElement, dir: DirectiveBinding) {
console.log('------updated-------', el, dir);
nextTick(() => {
overflowTooltip(el, dir);
});
},
beforeUnmount() {
console.log('------beforeUnmount-------');
},
unmounted() {
console.log('------unmounted-------');
},
};
3.局部组件中使用
先引入
import { vEllipsisTooltip } from '@/directives/tooltip';
模板中使用
<div class="inputBox flex-box" v-ellipsis-tooltip>
<!--el-select或者其他的元素-->
<el-select ... />
<div class="v-ellipsis-tooltip__container"></div>
</div>
4.全局组件使用
setupDirectives方法
import { App } from 'vue';
import { overflowTooltip } from './tooltip';
/**
* 注册全局自定义指令
* @param app
*/
export function setupDirectives(app: App) {
app.directive('ellipsisTooltip', overflowTooltip);
}
在main.ts中注册全局自定义指令
。。。
// 注册全局自定义指令
setupDirectives(app);
。。。
app.mount('#app');
不需要引入指令即可使用
TypeScript
模板中使用
<div class="inputBox flex-box" v-ellipsis-tooltip>
<!--el-select或者其他的元素-->
<el-select ... />
<div class="v-ellipsis-tooltip__container"></div>
</div>
5.利用插槽再次封装组件,指令也不需要重复写
MyTooltipWrapper组件:
TypeScript
<template>
<div :class="classes" v-ellipsis-tooltip>
<slot></slot>
<div class="v-overflow-tooltip__container"></div>
</div>
</template>
<script lang="ts" setup>
import { vEllipsisTooltip } from '../../directives/tooltip';
const props = defineProps<{
class: string | any[];
}>();
const { class: classes } = toRefs(props);
</script>
这个tooltip功能比较简单,不使用指令,直接在这个组件的基础上改也行:
TypeScript
<template>
<div :class="classes" ref="myRef" style="position: relative">
<slot></slot>
<MyTooltip :label="label" v-if="isOverflow" />
</div>
</template>
<script lang="ts" setup>
import { onUpdated } from 'vue';
import { get } from 'lodash-es';
import MyTooltip from './MyTooltip.vue';
const props = defineProps<{
class: string | any[];
}>();
const { class: classes } = toRefs(props);
const myRef = ref();
const label = ref('');
const isOverflow = ref<boolean>(false);
onMounted(() => {
nextTick(() => {
handleDataChange();
});
});
onUpdated(() => {
nextTick(() => {
handleDataChange();
});
});
function getIsOverflow(el: HTMLElement): boolean {
if (el) {
return el.scrollWidth > el.clientWidth;
}
return false;
}
function handleDataChange() {
const el = myRef.value;
const dom = el.querySelector('.xxx') || get(el.children, [0]); // 找到溢出文案的dom
isOverflow.value = getIsOverflow(dom);
label.value = get(dom, 'value') || get(dom, 'innerText') || '';
}
</script>
使用MyTooltipWrapper组件:
TypeScript
<MyTooltipWrapper class="inputBox flex-box">
<!--el-select或者其他的元素-->
<el-select ... />
</MyTooltipWrapper>