likes
comments
collection
share

div+css模拟实现Tooltip

作者站长头像
站长
· 阅读数 6

前言

最近公司有个需求,大致概括为如下:

  • 在一个编辑框内,新增某个特殊展示的内容(这个内容由JS手动创建标签)
  • 然后鼠标移到这个特殊内容上时,显示 Tooltip 效果
  • 一个编辑框内可能有多个特殊内容

实现后的效果其实就是这样:

div+css模拟实现Tooltip

由于以前的代码很老了,四五年前的代码,然后通过创建 span 标签包裹内容,替换原来的文本节点(因此有这个黄色的样式效果),所以不能直接包 antd 的 ToolTip,需要手动实现

实现思路

  • 通过 div + css 实现 ReplaceTextTooltip 组件(ToolTip 的样式效果)
  • 在最外层 div 容器上绑定 onMouseOver,判断是否移入特殊内容的DOM节点
  • 如果是,获取节点的坐标;如果不是,则重置 ReplaceTextTooltip 的位置
  • 计算偏移,把最终的位置传给 ReplaceTextTooltip

代码

最外层绑定 onMouseOver

// React 类组件
<div className='container'
    onMouseOver={(e) => {
       this.showReplaceAllTextPopover(e);
    }}
>
   //...
</div>

// 模拟 ToolTip 组件
<ReplaceTextTooltip
    elementInfo={popoverPositions}  // 位置
    content={popoverReplaceText} // hover时显示的内容
/>

onMouseover回调

  // 展示文本替代的 popover
  showReplaceAllTextPopover = (e) => {
    const { hoverReplaceTooltip } = this.state;
    if (
      e.target.className === 'eReplaceTextPronunciation_tag' &&
      !hoverReplaceTooltip
    ) {
      // 获取 DOM 节点的坐标和宽度
      const { left, top, width } = e.target.getBoundingClientRect();
      this.setState({
        // popoverPositions 用来透传给组件
        popoverPositions: {
          left,
          top,
          width,
        },
        hoverReplaceTooltip: true,
        popoverReplaceText: e.target.getAttribute('data-ereplacealltext') || '',
        popoverRenderDomText: e.target.innerText || '',
      });
    } else {
      // hoverReplaceTooltip 是判断当前是不是已经移入某个特殊DOM了
      if (hoverReplaceTooltip) {
        this.setState({
          popoverPositions: {
            left: 0,
            top: 0,
            width: 0,
          },
          hoverReplaceTooltip: false,
          popoverRenderDomText: '',
          popoverReplaceText: '',
        });
      }
    }
  };

ReplaceTextToolTip

import React, { FC, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import './style.less';

interface IProps {
  elementInfo: {
    left: number;
    top: number;
    width: number;
  };
  content: string;
}

const ReplaceTextTooltip: FC<IProps> = ({
  elementInfo = {
    left: 0,
    top: 0,
    width: 0,
  },
  content = '',
}) => {
  const contentRef = useRef<any>();
  const popContainerRef = useRef<any>();
  const [dealPosition, setDealPosition] = useState<{
    left: string;
    top: string;
  }>({
    left: '0px',
    top: '0px',
  });

  useEffect(() => {
    const { left, top, width } = elementInfo;
    if (left !== 0 && top !== 0) {
      const Left =
        left -
        (contentRef.current.offsetWidth
          ? contentRef.current.offsetWidth / 2
          : 0) +
        (contentRef.current.offsetWidth ? width / 2 : 0);
      const Top = top - (contentRef.current.offsetHeight > 33 ? 15 : 0) - 60;
      setDealPosition({
        left: `${Left}px`,
        top: `${Top}px`,
      });
    } else {
      setDealPosition({
        left: `0px`,
        top: `0px`,
      });
    }
  }, [elementInfo]);

  return ReactDOM.createPortal(
    <div className="absolute top-0 left-0 w-[100%]">
      <div>
        <div
          ref={popContainerRef}
          style={dealPosition}
          className="absolute border-box m-0 p-0 text-[#000000a5] text-[14px] leading-[1.5] list-none max-w-[250px] visible pb-md"
        >
          <div>
            <div className="replacePopover-arrow absolute left-[50%] translate-x-[-50%] bottom-[-5.071068px] w-[13.07106781px] h-[13.07106781px] block overflow-hidden pointer-events-none"></div>
            <div
              ref={contentRef}
              className="min-w-[30px] min-h-[32px] px-[8px] py-[6px] text-[#fff] text-center text-wrap bg-[#000000bf] rounded-[4px] shadow-[0 2px 8px rgba(0,0,0,.15)]"
            >
              <span>{content}</span>
            </div>
          </div>
        </div>
      </div>
    </div>,
    document.body,
  );
};

export default ReplaceTextTooltip;

其中,这段代码是使得 ToolTip 在 DOM 正上方显示的逻辑:

useEffect(() => {
    const { left, top, width } = elementInfo;
    if (left !== 0 && top !== 0) {
      const Left =
        left -
        (contentRef.current.offsetWidth
          ? contentRef.current.offsetWidth / 2
          : 0) +
        (contentRef.current.offsetWidth ? width / 2 : 0);
      const Top = top - (contentRef.current.offsetHeight > 33 ? 15 : 0) - 60;
      setDealPosition({
        left: `${Left}px`,
        top: `${Top}px`,
      });
    } else {
      setDealPosition({
        left: `0px`,
        top: `0px`,
      });
    }
  }, [elementInfo]);

这里面最重要的就是 Left 的计算,最开始我是直接使用 left

const Left = left;

但是效果确实这样的:

div+css模拟实现Tooltip

也就是 ToolTip 内容的开始位置和 DOM 的起始位置是一样的,所以需要计算在 X 轴的偏移量

计算 X 轴的偏移量

计算的思路是:

  • 首先让 ToolTip 向左偏移自己内容宽度的 1/2

div+css模拟实现Tooltip

  • 再让 ToolTip 向右偏移DOM的宽度 1/2

div+css模拟实现Tooltip

也就是

const Left =
        left -
        // contentRef 就是 ToolTip 自己
        (contentRef.current.offsetWidth 
          ? contentRef.current.offsetWidth / 2
          : 0) +
          // width 就是 DOM 的宽度
        (contentRef.current.offsetWidth ? width / 2 : 0);

最后

人为什么要上班