故事背景
- 手写一个元素拖拽的模板
- 作用是在子元素中为其某个父元素加上拖拽功能
- 之所以这么做是因为业务代码嵌套层数太深无法重构
- 文中的目标元素
XDom
和<EnableDrag />
的关系是父子关系
先看代码
import React from 'react';
import { injectIntl } from "react-intl";
import './EnableDrag.less';
interface IProps {
// 父组件控制此组件的可见性
visible: boolean;
intl: any;
}
interface IStates {
// 组件可见性
visible: boolean;
// 添加拖拽的目标dom的引用变量
XDom: HTMLDivElement;
// 鼠标是否落在XDom及其子元素上
isMouseDown: boolean;
// 鼠标最近一次落下的时候的位置
clientX: number;
clientY: number;
// XDom最新的left和top值
lastDownX: number;
lastDownY: number;
}
class EnableDrag extends React.Component<IProps, IStates> {
state = {
visible: false,
XDom: null,
isMouseDown: false,
clientX: 0,
clientY: 0,
lastDownX: 0,
lastDownY: 0,
}
constructor(props: IProps) {
super(props);
}
// 组件构建完毕之后找到目标元素然后将其引用存储在state的XDom变量中
componentDidMount(): void {
const XDom = document.querySelector('.target-draggable-dom') as HTMLDivElement;
this.setState({
XDom,
})
// 如果找到了目标元素那么就在window上绑定鼠标处理事件以完成拖拽功能
if (XDom) {
window.addEventListener('mousedown', this.handleMouseDown);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('mouseup', this.handleMouseUp);
// 如果鼠标到窗口外面则视为抬起
window.addEventListener('mouseleave', this.handleMouseUp);
}
}
// 组件卸载之前清除事件监听
componentWillUnmount(): void {
window.removeEventListener('mousedown', this.handleMouseDown);
window.removeEventListener('mousemove', this.handleMouseMove);
window.removeEventListener('mouseup', this.handleMouseUp);
window.removeEventListener('mouseleave', this.handleMouseUp);
}
// window监听了mousedown鼠标事件,当鼠标落下之后
// 首先判断落点是否是在XDom上
// 然后再做一些细致的判断:例如落点是在input框中,这个时候就不要有移动功能
handleMouseDown = (e) => {
if(!this.state.XDom.contains(e?.target)) return;
if(e?.target?.nodeName === 'INPUT') return;
e.stopPropagation();
if (!this.state.XDom) return;
// 得到鼠标落点位置
const { clientX, clientY } = e;
// 得到目标元素style上的left值和top值
const { left, top } = getComputedStyle(this.state.XDom);
const lastDownX = parseFloat(left);
const lastDownY = parseFloat(top);
// 更新位置信息
this.setState({
isMouseDown: true,
clientX,
clientY,
lastDownX,
lastDownY,
})
}
// 首先判断鼠标是否真的在目标元素上落下
// 然后根据初始位置和位移计算当前位置
// 使用transform移动元素,而不是随时修改style
handleMouseMove = (e) => {
e.stopPropagation();
// 如果鼠标没有真正落下就不再执行
if (!this.state.isMouseDown) return;
if (!this.state.XDom) return;
// 拿到当前鼠标落点
const { clientX, clientY } = e;
// 拿到目标元素的left和top值
const { clientX: lastX, clientY: lastY } = this.state;
// 计算位移值
const delX = clientX - lastX;
const delY = clientY - lastY;
// 使用transform更新位置
this.state.XDom.style.transform = `translate(${delX}px, ${delY}px)`;
}
// 当鼠标出界或者抬起之后需要将transform样式转换成style的left和top
handleMouseUp = (e) => {
e.stopPropagation();
if (!this.state.isMouseDown) return;
// 拿到此时鼠标位置
const { clientX, clientY } = e;
// 取出状态信息
const { clientX: lastX, clientY: lastY, lastDownX, lastDownY } = this.state;
// 计算鼠标抬起和落下的位移
const delX = clientX - lastX;
const delY = clientY - lastY;
// 计算新的left和top的值
const newPositionX = lastDownX + delX;
const newPositionY = lastDownY + delY;
// 更新样式,并且将transform:translate复原(这里直接复原到0 0了)
this.state.XDom.style.left = `${newPositionX}px`;
this.state.XDom.style.top = `${newPositionY}px`;
this.state.XDom.style.transform = `translate(${0}px, ${0}px)`;
// 修改isMouseDown的值表示鼠标抬起
this.setState({
isMouseDown: false,
})
}
render(): React.ReactNode {
const { visible } = this.props;
return (
<div
style={{ display: `${visible ? 'block' : 'none'}` }}
className='enableDrag'
>
<div
className='content-area'
style={{
position: 'relative',
}}
>
</div>
</div>
)
}
}
export default injectIntl(EnableDrag);
亮点总结
1. 使用contains判断鼠标落点是否在目标元素及其子元素上
2. 使用transform:translate和style:left/top搭配使拖拽更加丝滑
见:handleMouseMove
和handleMouseUp
方法的具体内容