基于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>
相关推荐
泥菩萨^_^2 小时前
【每天认识一个漏洞】React 和 Next.js RCE漏洞
前端·javascript·react.js
m0_471199632 小时前
【vue】收银界面离线可用,本地缓存订单,网络恢复后同步
网络·vue.js·缓存
1024肥宅2 小时前
JavaScript常用设计模式完整指南
前端·javascript·设计模式
董世昌412 小时前
js怎样控制浏览器前进、后退、页面跳转?
开发语言·前端·javascript
走,带你去玩3 小时前
uniapp live-pusher + 腾讯云直播
前端·javascript·uni-app
徐同保3 小时前
electron打包项目
前端·javascript·electron
Maybyy3 小时前
如何在项目里面添加一个可以左右翻动并显示指定日期的日历
前端·vue.js
前端 贾公子3 小时前
Vite 如何优化项目的图片体积
vue.js