关于一个拖拽移动的组件的封装

引言

在需求中有用到一个可拖拽排序,可编辑删除的卡片组件

适用范围

中台管理系统

组件实现

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
101
102
103
104
105
106
107
108
import React from 'react';
import Card from './Card';

class DragList extends React.Component {
constructor(props) {
super(props);
this.state = { ...props };
}

dragStart(e) {
this.dragged = e.currentTarget;
}
dragEnd(e) {
this.dragged.style.display = 'block';

e.target.classList.remove('drag-up');
// eslint-disable-next-line no-unused-expressions
this.over && this.over.classList.remove('drag-up');

e.target.classList.remove('drag-down');
// eslint-disable-next-line no-unused-expressions
this.over && this.over.classList.remove('drag-down');


let data = this.state.data;
const from = Number(this.dragged.dataset.id);
const to = Number(this.over.dataset.id);
console.log('from - to ', from, to);
data.splice(to, 0, data.splice(from, 1)[0]);
console.log('from - to - data ', data);
// set newIndex to judge direction of drag and drop
data = data.map((doc, index) => {
// eslint-disable-next-line no-param-reassign
doc.newIndex = index + 1;
return doc;
});

this.setState({ data });
this.props.onDragEndUpdate(data);
}

dragOver(e) {
// debugger
console.log(e.target);
e.preventDefault();

this.dragged.style.display = 'none';

if (e.target.tagName !== 'LI') {
return;
}

// 判断当前拖拽target 和 经过的target 的 newIndex

const dgIndex = JSON.parse(this.dragged.dataset.item).newIndex;
const taIndex = JSON.parse(e.target.dataset.item).newIndex;
const animateName = dgIndex > taIndex ? 'drag-up' : 'drag-down';


if (this.over && e.target.dataset.item !== this.over.dataset.item) {
this.over.classList.remove('drag-up', 'drag-down');
}

if (!e.target.classList.contains(animateName)) {
e.target.classList.add(animateName);
this.over = e.target;
}
}
render() {
// eslint-disable-next-line react/prop-types
const { onCardDelete, onCardEdit, onInputChange } = this.props;
const listItems = this.state.data.filter(q => q.type !== 'richText').map((item, i) => {
return (
<li
data-id={i}
key={i}
style={{ height: 'auto', margin: '10px 8.5%' }}
draggable="true"
onDragEnd={this.dragEnd.bind(this)}
onDragStart={this.dragStart.bind(this)}
data-item={JSON.stringify(item)}
>
<Card
value={item.value}
onInputChange={onInputChange}
newIndex={item.newIndex}
idx={i}
cardType={item.type}
cardTitle={item.cardTitle}
cardPrice={item.cardPrice}
onDelete={onCardDelete}
onEdit={onCardEdit}
cardImgId={item.cardImgId}
id={item.id}
packIdEn={item.packIdEn}
/>
</li>
);
});
return (
<ul onDragOver={this.dragOver.bind(this)} className="contain">
{listItems}
</ul>
);
}
}

export default DragList;

使用方法

1
2
3
4
5
6
7
8
<DragList
onInputChange={this.onCardInputChange}
key={this.state.dragListKey}
data={this.state.data}
onCardDelete={this.onCardDelete}
onCardEdit={this.onCardEdit}
onDragEndUpdate={this.onDragEndUpdate}
/>

外部属性

名称 类型 描述
data array 卡片内容数组
onInputChange function 拖拽组件内部卡片文本的编辑
key number 编辑 删除之后更新完数组之后 更新此值 重新渲染组件
onCardDelete function 删除事件
onCardEdit function 编辑事件
onDragEndUpdate function 拖拽完对数组的更新

内部方法

名称 类型 描述
dragStart function 拖拽开始 保存拖拽元素
dragOver function 拖拽时 设置拖拽元素样式和经过的元素样式 并记录互换的元素
dragEnd function 拖拽结束 清除过程中的样式 两项交换位置

具体代码可以到ck-lion-front/src/feature/content/addContent/dragList.js 下查阅

实现思路

通过onDragStart事件获取拖拽元素 onDragOver事件获取经过的最后一个元素 即需要置换位置的元素 然后在onDragEnd事件里面进行数组位置处理

不足

  • 封装的不够解耦 和业务代码耦合较多
  • 组件的动画效果方面有所欠缺
  • 组件的实现思路是对dom的操作 不够优雅
  • 不能支持移动端

重构方案

代码实现

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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
class DraggbleItem extends React.Component {
constructor(props) {
super(props);
this.state = {
// 索引
idx: null,
// 排序序号
no: null,
// 索引存储
val: null,
// 判断是不是已处于点击或拖拽状态
down: false,
deltaScale: 0.0,
v: 0,
clicked: 0,
posScale: 1,
z: 1,
y: 0,
posy: 0,
newPos: 0,
// y轴方向的阴影
yaxisShadow: 1,
// 模糊程度
shadowDim: 2,
deltaYaxisShadow: 0,
deltaShadowDim: 0,
ps1: 0,
ps2: 0,
};
// 绑定this
this.animate = this.animate.bind(this);
...
...
this.handleTouchMove = this.handleTouchMove.bind(this);
}

componentWillMount() {
this.setState({
idx: this.props.idx,
val: this.props.idx,
y: this.props.idx * 100,
no: this.props.idx,
});
}

componentDidMount() {
// 注册动画
this.startLoop();
}

componentWillReceiveProps(newProps) {
if (!this.state.down) {
this.setState({
idx: newProps.idx,
val: newProps.idx,
});
}
}

componentWillUnmount() {
// 解绑动画
this.stopLoop();
}

// pc的开始托转
handleDown(e) {
this.setState({
down: true,
z: 4,
posy: e.clientY,
});
}
// 移动端开始拖拽
handleTouchStart(e) {
e.persist();
this.setState({
down: true,
z: 4,
posy: e.touches[0].screenY,
});
}
// pc结束拖拽
handleUp() {
this.setState({
down: false,
});
}
// 移动端结束拖拽
handleTouchEnd() {
this.setState({
down: false,
});
}
// pc拖拽过程
handleMove(e) {
this.moveHandler(e,'move')
}
// 移动端拖拽过程
handleTouchMove(e) {
e.persist();
this.moveHandler(e,'touch')
}
// 根据type判断pc还是移动端
getTargetByType=(e,type)=>{
return type==='touch'? e.touches[0].screenY:e.clientY
}
moveHandler = (e, type) => {
// 如果已经点击
if (this.state.down) {
// 移动的距离 减去初始距离
let newPos = this.getTargetByType(e, type) - this.state.posy;
// y是最终距离 val是第几个方块
this.setState(
{
y: this.state.val * 100 + newPos,
},
() => {
// 如果移动的距离 大于一半距离 下移
console.log('this.state.y', this.state.y, 'this.state.idx * 100', this.state.idx * 100);
if (this.state.y > this.state.idx * 100 + 50) {
if (this.state.idx + 1 < this.props.cardLength) {
this.setState(
{
idx: this.state.idx + 1,
},
() => {
this.props.handlePos(
this.state.no,
this.state.idx,
this.state.idx - 1,
);
},
);
}
// 上移
} else if (this.state.y < this.state.idx * 100 - 50) {
if (this.state.idx - 1 > -1) {
this.setState(
{
idx: this.state.idx - 1,
},
() => {
this.props.handlePos(
this.state.no,
this.state.idx,
this.state.idx + 1,
);
},
);
}
}
},
);
}
};

// 更新拖拽缩放大小
updateScale() {
..........
}
// 更新拖拽时y方向的位置
updateYaxis() {
..........
}

// 动画开始
startLoop() {
if (!this._frameId) {
this._frameId = window.requestAnimationFrame(this.animate);
}
}

animate() {
this.updateScale();
this.updateYaxis();
this.updateShadow();
const cardDom = document.getElementById('card' + this.state.no);
cardDom && (cardDom.style.transform =
' translate3d(0px ,' +
this.state.y +
'px, 0px)scale(' +
this.state.posScale +
')');
cardDom && (cardDom.style.zIndex = this.state.z);
cardDom && (cardDom.style.boxShadow =
'rgba(0, 0, 0, 0.2) 0px ' +
this.state.yaxisShadow +
'px ' +
this.state.shadowDim +
'px 0px');
this.frameId = window.requestAnimationFrame(this.animate);
}

stopLoop() {
window.cancelAnimationFrame(this._frameId);
}

render() {
return (
<div
id={'card' + this.state.no}
className={draggable.card}
onMouseDown={this.handleDown}
onMouseUp={this.handleUp}
onMouseMove={this.handleMove}
onMouseLeave={this.handleUp}
onTouchStart={this.handleTouchStart}
onTouchMove={this.handleTouchMove}
onTouchEnd={this.handleTouchEnd}
>
{this.props.children}
<div className={draggable.close} onClick={this.props.onDelete(this.state.no)}></div>
</div>
);
}
}
  • 这个组件是在codepen上面找的 自己再结合实际改造了一下
  • 用了requestAnimationFrame来实现动画效果 很流畅
  • 对于一些不支持的浏览器可能要做向下兼容

效果图

learnUmi.mp4 (291.74KB)

后记

时间仓促 还有很多其他的组件特性没考虑到 后续还会做出进一步的优化

查看评论