组件库开发—— tooltip组件 | 青训营笔记

820 阅读3分钟

这是我参与「第五届青训营」伴学笔记创作活动的第 16 天

tooltip组件

tooltip组件和message组件有点点类似,但是这个组件只需要套在别的组件外面即可。实现起来也不难,但是很多坐标处理是比较麻烦的。

接口

接口内容其实很简单,直接看注释即可

import { ExtractPropTypes } from "vue";

export const TooltipType = [
  "left-start",
  "left",
  "left-end",
  "top-start",
  "top",
  "top-end",
  "right-start",
  "right",
  "right-end",
  "bottom-start",
  "bottom",
  "bottom-end",
];

export const TooltipProps = {
  placement: {   // 摆放位置
    type: String,
    values: TooltipType,
  },
  modelValue: {  // 响应式渲染显示/隐藏
    type: Boolean,
    default: null,
  },
  content: [String, Number],  // 文本内容
  width: String,
};

export type TooltipProps = ExtractPropTypes<typeof TooltipProps>;

创建节点

首先我们需要在组件中获取到嵌入的节点,这显然就是插槽来做了。再vue我们不能直接用slot,需要在外面套一个标签,这里最好套个span标签,这样也不会影响文档流的布局。

<template>
  <span>
    <slot></slot>
  </span>
</template>

然后我再来解释为什么要获取到嵌入的节点,因为我们实际的实现思路是需要获取到节点的位置,然后在这个节点的位置挂上一个div来渲染的。

首先我们创建一个节点,并给这个节点挂上我们准备好的样式,并把节点内容挂载watch中。

// 获取当前组件的实例
tip = document.createElement("div");
tip.className = `h-tooltip h-tooltip-${placement!.value}`;
width?.value && (tip.style.width = width.value);
tid = tip.id = `h-tooltip-${instance!.uid}`;

watchEffect(() => {
    tip.innerHTML = `<span>${content?.value}</span>` || "";
})

渲染节点

紧接着,我们创建完节点后,我们需要考虑如何给这个节点渲染出来并按照特定的位置安放。

由于我们需要操作到节点,在vue异步渲染模式下,我们需要在nextTick中才能获取到,同样需要加入到监听中。

// 核心函数,获取到已经渲染完毕的DOM.
function update(tip: any, tid: string): any {
  // 默认取得插件内第一个元素,并获取它的坐标
  const Rect = instance?.proxy?.$el.firstElementChild.getBoundingClientRect();
  const el = document.getElementById(tid);
  if (!el) {
    document.body.appendChild(tip);
  }
  tip.style.display = "block";
  const { x, y } = calcStyle(Rect, tip, placement?.value!);
  tip.style.top = y + "px";
  tip.style.left = x + "px";
}

watchEffect(() => {
    tip.innerHTML = `<span>${content?.value}</span>` || "";
    nextTick(update(tip, tid));
})

这个渲染函数,就是我们把插槽中的节点获取到,求出坐标系中的坐标,在通过calcStyle获取到对应的节点坐标,挂载在我们创建的节点样式中,最后加入到body后面。

获取坐标位置 & 开发逻辑

坐标获取

位置节点坐标有12种,由 placement 属性决定展示效果: placement属性值为:[方向]-[对齐位置];四个方向:topleftrightbottom;三种对齐位置:startend,默认为空。 如 placement="left-end",则提示信息出现在目标元素的左侧,且提示信息的底部与目标元素的底部对齐。

function calcStyle(Rect: any, tip: any, key: string): { x: number; y: number } {
  let y = document.documentElement.scrollTop;
  let x = 0;
  const placement: any = {
    "top-start": () => {
      x += Rect.x;
      y += Rect.y - tip.offsetHeight;
    },
    top: () => {
      x += Rect.x + (Rect.width - tip.offsetWidth) * 0.5;
      y += Rect.y - tip.offsetHeight;
    },
    "top-end": () => {
      x += Rect.x + Rect.width - tip.offsetWidth;
      y += Rect.y - tip.offsetHeight;
    },
    "left-start": () => {
      x += Rect.x - tip.offsetWidth;
      y += Rect.y;
    },
    left: () => {
      x += Rect.x - tip.offsetWidth;
      y += Rect.y + (Rect.height - tip.offsetHeight) * 0.5;
    },
    "left-end": () => {
      x += Rect.x - tip.offsetWidth;
      y += Rect.y + Rect.height - tip.offsetHeight;
    },
    "right-start": () => {
      x += Rect.x + Rect.width;
      y += Rect.y;
    },
    right: () => {
      x += Rect.x + Rect.width;
      y += Rect.y + (Rect.height - tip.offsetHeight) * 0.5;
    },
    "right-end": () => {
      x += Rect.x + Rect.width;
      y += Rect.y + Rect.height - tip.offsetHeight;
    },
    "bottom-start": () => {
      x += Rect.x;
      y += Rect.y + Rect.height;
    },
    bottom: () => {
      x += Rect.x + (Rect.width - tip.offsetWidth) * 0.5;
      y += Rect.y + Rect.height;
    },
    "bottom-end": () => {
      x += Rect.x + Rect.width - tip.offsetWidth;
      y += Rect.y + Rect.height;
    },
  };
  placement[key]();
  return { x, y };
}

显示与隐藏

最后我们需要在hover触发后,才显示节点。这个逻辑就很简单了,我们只需要为节点挂载上两个监听事件mouseentermouseleave响应式更新即可。

// 监听有无接触组件,接触了为true,离开为false
el &&
el.addEventListener("mouseenter", () => {
  isShow.value = true;
});

el &&
el.addEventListener("mouseleave", () => {
  setTimeout(() => {
    isShow.value = false;
  }, 500);
});

同样的,我们也需要在watch中处理响应式触发的情况。

watchEffect(() => {
    ...

    if (isShow.value) {
      show(tip);
    } else {
      hide(tid);
    }
});
  
  // 隐藏tooltip
function hide(tid: string) {
  const el = document.getElementById(tid);
  if (el) {
    // 移除样式
    el.classList.remove("h-tooltip-show");
  }
}

// 显示tooltip
function show(tip: any) {
  tip && tip.classList.add("h-tooltip-show");
}

在实现完成后,我们的页面就会挂载上tooltip节点,但是我们在其它页面不需要它,因此我们需要在实现页面更替时删掉dom原生,防止多个节点出现冗余。 image.png

// 退出页面后删除全部的结点
onUnmounted(() => {
    const el = document.getElementById(tid);
    el && document.body.removeChild(tip);
});

最后实现一个经典的tooltip展示

<template>
  <div class="Container">
    <div class="row center">
      <h-tooltip class="box-item" content="top-start" placement="top-start">
        <h-button type="default" plain>top-start</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="top" placement="top">
        <h-button type="default" plain>top</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="top-end" placement="top-end">
        <h-button type="default" plain>top-end</h-button>
      </h-tooltip>
    </div>
    <div class="row">
      <h-tooltip class="box-item" content="left-start" placement="left-start">
        <h-button type="default" plain>left-start</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="right-start" placement="right-start">
        <h-button type="default" plain>right-start</h-button>
      </h-tooltip>
    </div>
    <div class="row">
      <h-tooltip class="box-item" content="left" placement="left">
        <h-button type="default" plain>left</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="right" placement="right">
        <h-button type="default" plain>right</h-button>
      </h-tooltip>
    </div>
    <div class="row">
      <h-tooltip class="box-item" content="left-end" placement="left-end">
        <h-button type="default" plain>left-end</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="right-end" placement="right-end">
        <h-button type="default" plain>right-end</h-button>
      </h-tooltip>
    </div>
    <div class="row center">
      <h-tooltip class="box-item" content="bottom-start" placement="bottom-start">
        <h-button type="default" plain>bottom-start</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="bottom" placement="bottom">
        <h-button type="default" plain>bottom</h-button>
      </h-tooltip>
      <h-tooltip class="box-item" content="bottom-end" placement="bottom-end">
        <h-button type="default" plain>bottom-end</h-button>
      </h-tooltip>
    </div>
  </div>
</template>

<style>
.Container {
  width: 600px;
}
.Container .row {
  display: flex;
  align-items: center;
  justify-content: space-between;
}

.Container .center {
  justify-content: center;
}

.Container .box-item {
  margin: 10px;
}
</style>

image.png

补充

最后附上完整代码地址:

github项目代码

tooltip组件演示效果

如果觉得有帮助的话,请给我们项目点个star吧。