基于Vue 3和Element Plus利用h、render函数写一个简单的tooltip局部or全局指令

一、需求背景:

tooltip是前端开发中一个常见的功能,当文案内容超出一行会要求一行显示末尾显示省略号,在其右边显示一个info图标,hover这个info图标显示完整文案的tooltip,本文就是简单写一个指令实现这个效果

二、框架版本

Vue3版本:

"vue": "^3.0.0"

Element Plus版本:

"element-plus": "^2.3.12"

三、参考资料地址:

https://cn.vuejs.org/api/render-function.html#h

https://element-plus.org/en-US/component/icon#api

四、实现过程:

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>
相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60613 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅4 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅4 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment4 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax