前言
最近公司有个需求,大致概括为如下:
- 在一个编辑框内,新增某个
特殊展示
的内容(这个内容由JS手动创建标签) - 然后鼠标移到这个特殊内容上时,显示 Tooltip 效果
- 一个编辑框内可能有多个特殊内容
实现后的效果其实就是这样:
由于以前的代码很老了,四五年前的代码,然后通过创建 span 标签包裹内容,替换原来的文本节点(因此有这个黄色的样式效果),所以不能直接包 antd 的 ToolTip,需要手动实现
实现思路
- 通过 div + css 实现
ReplaceTextTooltip
组件(ToolTip 的样式效果) - 在最外层 div 容器上绑定
onMouseOver
,判断是否移入特殊内容的DOM节点 - 如果是,获取节点的坐标;如果不是,则重置 ReplaceTextTooltip 的位置
- 计算偏移,把最终的位置传给 ReplaceTextTooltip
代码
最外层绑定 onMouseOver
tsx
// React 类组件
<div className='container'
onMouseOver={(e) => {
this.showReplaceAllTextPopover(e);
}}
>
//...
</div>
// 模拟 ToolTip 组件
<ReplaceTextTooltip
elementInfo={popoverPositions} // 位置
content={popoverReplaceText} // hover时显示的内容
/>
onMouseover回调
tsx
// 展示文本替代的 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
tsx
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 正上方显示的逻辑:
tsx
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
tsx
const Left = left;
但是效果确实这样的:
也就是 ToolTip 内容的开始位置和 DOM 的起始位置是一样的,所以需要计算在 X 轴的偏移量
计算 X 轴的偏移量
计算的思路是:
- 首先让 ToolTip 向左偏移自己内容宽度的 1/2
- 再让 ToolTip 向右偏移DOM的宽度 1/2
也就是
tsx
const Left =
left -
// contentRef 就是 ToolTip 自己
(contentRef.current.offsetWidth
? contentRef.current.offsetWidth / 2
: 0) +
// width 就是 DOM 的宽度
(contentRef.current.offsetWidth ? width / 2 : 0);
最后
人为什么要上班