基于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>
相关推荐
Justin3go19 小时前
HUNT0 上线了——尽早发布,尽早发现
前端·后端·程序员
怕浪猫20 小时前
第一章 JSX 增强特性与函数组件入门
前端·javascript·react.js
铅笔侠_小龙虾20 小时前
Emmet 常用用法指南
前端·vue
钦拆大仁20 小时前
跨站脚本攻击XSS
前端·xss
前端小L20 小时前
贪心算法专题(十):维度权衡的艺术——「根据身高重建队列」
javascript·算法·贪心算法
VX:Fegn08951 天前
计算机毕业设计|基于springboot + vue校园社团管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
Fortunate Chen1 天前
类与对象(下)
java·javascript·jvm
ChangYan.1 天前
直接下载源码但是执行npm run compile后报错
前端·npm·node.js
skywalk81631 天前
在 FreeBSD 上可以使用的虚拟主机(Web‑Hosting)面板
前端·主机·webmin