基于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>
相关推荐
二哈喇子!4 小时前
BOM模型
开发语言·前端·javascript·bom
二哈喇子!4 小时前
Vue2 监听器 watcher
前端·javascript·vue.js
yanyu-yaya5 小时前
前端面试题
前端·面试·前端框架
二哈喇子!5 小时前
使用NVM下载Node.js管理多版本
前端·npm·node.js
GGGG寄了6 小时前
HTML——文本标签
开发语言·前端·html
摘星编程6 小时前
在OpenHarmony上用React Native:ActionSheet确认删除
javascript·react native·react.js
2501_944521596 小时前
Flutter for OpenHarmony 微动漫App实战:推荐动漫实现
android·开发语言·前端·javascript·flutter·ecmascript
Amumu121387 小时前
Vue核心(三)
前端·javascript·vue.js
CoCo的编程之路7 小时前
2026 前端效能革命:如何利用智能助手实现“光速”页面构建?深度横评
前端·人工智能·ai编程·comate·智能编程助手·文心快码baiducomate
RFCEO7 小时前
HTML编程 课程五、:HTML5 新增语义化标签
前端·html·html5·跨平台·语义化标签·可生成安卓/ios·html最新版本