本文意在记录一次实现拖拽封装的过程,不保证文中存在纰漏,欢迎交流学习哦~
拖拽的基本实现
首先需要了解到,拖拽所使用到的相关事件:
PC端:onmousedown、 onmousemove、onmouseup
移动端:ontouchstart、ontouchmove、ontouchend
pc端和移动端的实现有部分差别,原理基本一致。以下先单独讲PC端的拖拽实现:
第一步:将onmousedown绑定到目标元素上,获取当前的偏移值
element.onmousedown = function (e) {
e = e || window.e;
// 偏移位置 = 元素的X - 元素的offset
let disX = e.clientX - element.offsetLeft;
let disY = e.clientY - element.offsetTop;
}
在元素上按下鼠标,可以查看到onmousedown的所有事件属性。拖拽需要使用其中的clientX(水平坐标)和clientY(垂直坐标)。
除此之外还需要获取目标元素的offsetLeft(距离左偏移的值)和offsetTop(距离顶部偏移的值),用于在每次onmousedown的时候,获取当前偏移位置,这是一个相对值,将在onmousemove中使用。
第二步:将onmousemove绑定到document上,在onmousedown中执行
document.onmousemove = function(e) {
e = e || window.e;
// 元素位置 = 现在鼠标位置 - 元素偏移值
let left = e.clientX - disX;
let top = e.clientY - disY;
}
在按下鼠标移动的时候,同样需要取到当前新的clientX和clientY,每当鼠标拖动到新的位置,就重新计算现在的鼠标位置 - 初始的元素偏移值,得元素新的位置坐标,这就拖拽的基本原理。
但现在的实现,存在拖动元素会超出当前的可视窗口的情况,因此需要在onmousemove中设置可拖动的范围。此外需要注意的是,onmousemove方法需要在onmousedown中使用,保证disX和disY不被销毁,onmousemove中能正常取到初始的偏移值。具体实现将在最后的完整版代码中展示。
第三步:结束拖拽
document.onmouseup = function(){
document.onmousemove = null;
document.onmouseup = null;
}
鼠标抬起时,元素不再需要跟随鼠标移动,要将onmousemove和onmouseup 事件清除,使用赋值为null的方式即可。如果使用addEventListener绑定事件,需要使用removeEventListener解绑事件。
移动端实现方式的区别
touch事件需都绑定在目标元素上,通过取得changedTouches[0]中的pageX和pageY计算新的坐标。其余的实现原理基本一致。
在react中封装拖拽
var isMoblie = 'ontouchstart' in window; // 是否为移动端
class Drag extends React.Component {
constructor(props) {
super(props);
// props传递配置项
this.elementWid = props.width || 100;
this.elementHeight = props.height || 100;
this.left = props.left || 0;
this.top = props.top || 0;
this.zIndex = props.zIndex || 0;
this.clientWidth = props.maxWidth;
this.clientHeight = props.maxHeight;
this._dragStart = this.dragStart.bind(this);
this.state = {
left: this.left,
top: this.top
};
}
dragStart(ev) {
let target = ev.target;
if(isMoblie && ev.changedTouches) {
this.startX = ev.changedTouches[0].pageX;
this.startY = ev.changedTouches[0].pageY;
} else {
this.startX = ev.clientX;
this.startY = ev.clientY;
}
// 偏移位置 = 鼠标的初始值 - 元素的offset
this.disX = this.startX - target.offsetLeft;
this.disY = this.startY - target.offsetTop;
this.zIndex += 1;
this._dragMove = this.dragMove.bind(this);
this._dragEnd = this.dragEnd.bind(this);
if(!isMoblie) {
document.addEventListener('mousemove', this._dragMove, false);
document.addEventListener('mouseup', this._dragEnd, false);
}
}
dragMove(ev) {
if(isMoblie && ev.changedTouches) {
this.clientX = ev.changedTouches[0].pageX;
this.clientY = ev.changedTouches[0].pageY;
} else {
this.clientX = ev.clientX;
this.clientY = ev.clientY;
}
// 元素位置 = 现在鼠标位置 - 元素的偏移值
let left = this.clientX - this.disX;
let top = this.clientY - this.disY;
// 处理不可超出规定拖拽范围
if (left < 0) {
left = 0;
}
if (top < 0) {
top = 0;
}
if (left > this.clientWidth - this.elementWid) {
left = this.clientWidth - this.elementWid;
}
if (top > this.clientHeight - this.elementHeight) {
top = this.clientHeight - this.elementHeight;
}
this.setState({
left: left,
top: top
});
}
dragEnd(e) {
const {onDragEnd} = this.props;
document.removeEventListener('mousemove', this._dragMove);
document.removeEventListener('mouseup', this._dragEnd);
onDragEnd && onDragEnd({
X: this.startX - this.clientX,
Y: this.startY - this.clientY
})
}
render() {
const { className, width, height} = this.props;
const { left, top } = this.state;
let styles = {
width,
height
}
// 根据组件配置,为元素添加对应的样式
if(this.props.left) {
styles['left'] = this.state.left;
}
if(this.props.top) {
styles['top'] = this.state.top;
}
if (this.props.zIndex) {
styles['zIndex'] = this.zIndex;
}
const cls = classnames('dragbox', {
[className]: !!className
})
return (
<div
className = {cls}
onTouchStart = {this._dragStart}
onTouchMove = {(e)=>this._dragMove(e)}
onTouchEnd = {this._dragEnd}
onMouseDown = {this._dragStart}
onMouseUp = {this._dragEnd}
style={styles}
ref="dragElement"
>
{this.props.children}
</div>
)
}
}
export default Drag;
Drag是一个类,在它的构造函数中存放着配置项,这些配置项只能被当前实例访问,每次实例化这个类,构造函数中的值都会被重新创建一次。因此,将基本配置项放在其中较为合理。
state中的值每次修改都会重新render虚拟DOM,而拖拽需要的是不断改变元素的位置,因此将位置信息left和top放在state中处理较为合理。由于state的特性,其他值尽量可以不放在state中处理,从而避免高频率渲染。
以上的封装基本实现了移动端和PC端拖拽的兼容。封装的组件配置项有:目标元素的宽度和高度、拖拽方向的选择left和top、目标元素的层级、目标元素的拖拽范围。如在运行过程中发现问题,还请斧正。