My Little World

拖拽

使用 React 实现拖放的技术要点

1.如何使用 React 的鼠标事件系统
2.如何判断拖放开始和拖放结束
3.如何实现拖放元素的位置移动 (可分为两种,一种是直接拖着具体要被移动的dom移动;另外一种是具体dom留在原位,拖着具体dom的影子移动,确定位置后,再将具体的dom放过去)
4.拖放状态在组件中如何维护

鼠标移动可能会超出要移动的组件和他的父组件,除了在document上监听,还可以在一个全局透明遮罩层上监听MouseMove和MouseUp好处:拖放过程不会选中其他任何元素,防止点击到其他组件

其他情景思考:
每个条目高度不一致,如何确定移动位置?
条目所在的列表有折叠,存在滚动条,如何根据滚动条确定位置?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100

import React, { Component } from "react";

require("./DndSample.css");

const list = [];
for (let i = 0; i < 10; i++) {
list.push(`Item ${i + 1}`);
}

const move = (arr, startIndex, toIndex) => {
arr = arr.slice();
arr.splice(toIndex, 0, arr.splice(startIndex, 1)[0]);
return arr;
};

const lineHeight = 42;
class DndSample extends Component {
constructor(props) {
super(props);
this.state.list = list;
}
state = {
dragging: false,
draggingIndex: -1,
startPageY: 0,
offsetPageY: 0,
};

handleMounseDown = (evt, index) => {
this.setState({
dragging: true,
startPageY: evt.pageY,
currentPageY: evt.pageY,
draggingIndex: index,
});
};
handleMouseUp = () => {
this.setState({ dragging: false, startPageY: 0, draggingIndex: -1 });
};
//如果往下滑,就一次把下一条数据交换位置,如果往上移动,就一次把上一条数据交换位置,
handleMouseMove = evt => {
let offset = evt.pageY - this.state.startPageY;
const draggingIndex = this.state.draggingIndex;
if (offset > lineHeight && draggingIndex < this.state.list.length - 1) {
// move down
offset -= lineHeight;
this.setState({
list: move(this.state.list, draggingIndex, draggingIndex + 1),
draggingIndex: draggingIndex + 1,
startPageY: this.state.startPageY + lineHeight,
});
} else if (offset < -lineHeight && draggingIndex > 0) {
// move up
offset += lineHeight;
this.setState({
list: move(this.state.list, draggingIndex, draggingIndex - 1),
draggingIndex: draggingIndex - 1,
startPageY: this.state.startPageY - lineHeight,
});
}
this.setState({ offsetPageY: offset });
};

getDraggingStyle(index) {
if (index !== this.state.draggingIndex) return {};
return {
backgroundColor: "#eee",
transform: `translate(10px, ${this.state.offsetPageY}px)`,
opacity: 0.5,
};
}

render() {
return (
<div className="dnd-sample">
<ul>
{this.state.list.map((text, i) => (
<li
key={text}
onMouseDown={evt => this.handleMounseDown(evt, i)}
style={this.getDraggingStyle(i)}
>
{text}
</li>
))}
</ul>
{this.state.dragging && ( //在一个遮罩层上监听MouseMove和MouseUp
<div
className="dnd-sample-mask"
onMouseMove={this.handleMouseMove}
onMouseUp={this.handleMouseUp}
/>
)}
</div>
);
}
}

export default DndSample;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.dnd-sample ul {
display: inline-block;
margin: 0;
padding: 0;
background-color: #eee;
}

.dnd-sample li {
cursor: default;
list-style: none;
border-bottom: 1px solid #ddd;
padding: 10px;
margin: 0;
width: 300px;
background-color: #fff;
}

.dnd-sample-mask {
position: fixed;
left: 0;
right: 0;
top: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.1);
}