react-dnd 简析 2
上篇文章有说到场景:卡片在不同的容器之间来回拖动。下面,对上篇文章中的代码整理一下:
上期代码
1
2
3
4
// ItemTypes
const ItemTypes = {
CARD : 'Card'
}
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
// Card
const Card = React . Component {
constructor ( props ) {
super ( props );
}
render () {
const { children , id , text , connectDragSource } = this . props ;
return connectDragSource (
< div className = { styles . card } >
{ text }
< /div>
);
}
}
const cardSource = {
beginDrag ( props ) {
return {
id : props . id ,
index : props . index ,
text : props . text ,
boxId : props . boxid
};
},
isDragging ( props , monitor ) {
return props . id === monitor . getItem (). id ;
}
};
function dragCollect ( connect , monitor ) {
return {
connectDragSource : connect . dragSource (),
isDragging : monitor . isDragging ()
};
}
let DragCard = DragSource ( ItemTypes . CARD , cardSource , dragCollect )( Card )
export default DragCard
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
// CardBox
class CardBox extends React . Component {
constructor ( props ) {
super ( props );
}
render () {
const { children , connectDropTarget } = this . props ;
return connectDropTarget (
< div className = { styles . cardBox } >
{ children }
< /div>
);
}
}
const cardBoxTarget = {
hover ( props , monitor , component ) {
const hoverCard = monitor . getItem ();
if ( hoverCard . id && props . boxid != card . boxId ){
props . changeCard ( hoverCard . index , props . boxid );
}
}
};
function dropCollect ( connect , monitor ) {
return {
connectDropTarget : connect . dropTarget ()
};
}
let DropCardBox = DropTarget ( ItemTypes . CARD , cardBoxTarget , dropCollect )( CardBox )
export default DropCardBox
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
// Kanban
class Kanban extends React . Component {
constructor ( props ) {
super ( props );
this . state = {
cards : [
{ id : 1 , text : 'card 1' , boxId : 1 },
{ id : 2 , text : 'card 2' , boxId : 1 },
{ id : 3 , text : 'card 3' , boxId : 2 },
{ id : 4 , text : 'card 4' , boxId : 2 },
],
boxes : [
{ id : 1 , boxId : 1 },
{ id : 2 , boxId : 2 }
]
}
}
changeCard ( index , newBoxId ) {
const dragCard = this . state . cards [ index ]
if ( ! dragCard ){ return }
dragCard . boxId = newBoxId
this . setState ( update ( this . state , {
cards : {
$splice : [
[ index , 1 ],
[ index , 0 , dragCard ]
]
}
}));
}
render () {
return (
< div >
{ this . state . boxes . map ( box => {
return (
< CardBox
key = { 'cb-' + box . boxId }
changeCard = { this . changeCard }
boxid = { box . boxId } >
{ this . state . cards . map ( ( card , index ) => {
if ( box . boxId == card . boxId )
return (
< Card
key = { 'c-' + card . id }
id = { card . id }
index = { index }
boxid = { card . boxId }
text = { card . text }
/>
)
else return '' ;
})}
< /CardBox>
)
})}
< /div>
);
}
}
export default DragDropContext ( HTML5Backend )( Kanban )
这四个文件,ItemTypes.js
/Card.js
/CardBox.js
/Kanban.js
为基本的模型文件,样式文件应该也要有的,这里就贴了。
新需求
对于看板来说,不同容器间的来回拖动是必要的。同时,如果想在单个容器中上下拖动,又需要怎么实现?
由于上篇文章的changeCard(index, newBoxId)
只能调整卡片的boxId
的属性,并不能改变其同一容器中的上下位置,所以需要调整一下,
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
// 增加一个参数 newIndex
changeCard ( index , newBoxId , newIndex ) {
const dragCard = this . state . cards [ index ]
if ( ! dragCard ){ return }
if ( newBoxId && dragCard . boxId != newBoxId ) {
dragCard . boxId = newBoxId
this . setState ( update ( this . state , {
cards : {
$splice : [
[ index , 1 ],
[ index , 0 , dragCard ]
]
}
}));
} else if ( newIndex && index != newIndex ) {
// newIndex 当卡片拖动到某个卡片的上面时,
// newIndex 就是此某个卡片的 index
// 将拖动的卡片 剪切到 此位置上
//
// dragCard.index ~= newIndex
this . setState ( update ( this . state , {
cards : {
$splice : [
[ index , 1 ],
[ newIndex , 0 , dragCard ]
]
}
}))
}
}
通过调整 index
的值来达到位置上的变化。
当卡片拖动到另一卡片上面时,需要获取到这一卡片的 index
的值,再将拖动卡片设置到新的位置上。
那么,需要将Card
设置为可接受的容器DropTarget
。
参考之前的CardBox
,
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
const cardTarget = {
hover ( props , monitor , component ) {
const dragIndex = monitor . getItem (). index ;
const hoverIndex = props . index ;
// Don't replace items with themselves
if ( dragIndex === hoverIndex ) {
return ;
}
// Determine rectangle on screen
const hoverBoundingRect = findDOMNode ( component ). getBoundingClientRect ();
// Get vertical middle
const hoverMiddleY = ( hoverBoundingRect . bottom - hoverBoundingRect . top ) / 2 ;
// Determine mouse position
const clientOffset = monitor . getClientOffset ();
// Get pixels to the top
const hoverClientY = clientOffset . y - hoverBoundingRect . top ;
// Only perform the move when the mouse has crossed half of the items height
// When dragging downwards, only move when the cursor is below 50%
// When dragging upwards, only move when the cursor is above 50%
// Dragging downwards
if ( dragIndex < hoverIndex && hoverClientY < hoverMiddleY ) {
return ;
}
// Dragging upwards
if ( dragIndex > hoverIndex && hoverClientY > hoverMiddleY ) {
return ;
}
// Time to actually perform the action
props . changeCard ( dragIndex , null , hoverIndex );
// Note: we're mutating the monitor item here!
// Generally it's better to avoid mutations,
// but it's good here for the sake of performance
// to avoid expensive index searches.
monitor . getItem (). index = hoverIndex ;
}
}
以上参考官方的例子,通过 component
来获取纵向属性,来准确得到 newIndex
参数。
当拖动的高度没有达到卡片一半时,不会触发changeCard(..)
方法。
1
2
3
4
5
6
7
function dropCollect ( connect , monitor ) {
return {
connectDropTarget : connect . dropTarget ()
};
}
let DropCard = DropTarget ( ItemTypes . CARD , cardTarget , dropCollect )( Card );
dropCollect
基本是一样的。
那么,完成这点之后,Card
是可拖动的,又是可以容器,
所以,
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
// Card
const Card = React . Component {
constructor ( props ) {
super ( props );
}
render () {
const { children , id , text , isDragging , connectDragSource , connectDropTarget } = this . props ;
const opacity = isDragging ? 0.5 : 1 ;
return connectDragSource ( connectDropTarget (
< div className = { styles . card }
style = { { opactiy : opacity } }
>
{ text }
< /div>
));
}
}
const cardSource = {
//...
};
const cardTarget = {
// ...
}
function dragCollect ( connect , monitor ) {
//...
}
function dropCollect ( connect , monitor ) {
// ...
}
let DropCard = DropTarget ( ItemTypes . CARD , cardTarget , dropCollect )( Card );
let DragCard = DragSource ( ItemTypes . CARD , cardSource , dragCollect )( DropCard )
export default DragCard
通过最后处绑定,Card
的props
会多出来个值来connectDragSource
/connectDropTarget
。
在Card
类中,根据 isDragging
来判断设置卡片的透明度,这就是为什么之前要重新定义CardSource
中的isDragging
方法的缘由。
1
2
3
4
5
6
7
8
const cardSource = {
beginDrag ( props ) {
// ...
},
isDragging ( props , monitor ) {
return props . id === monitor . getItem (). id ;
}
};
因为,当一个拖动卡片的index
为1
时,dnd
默认会其标识为isDragging=true
。而,拖动卡片到index=0
时,由于changeCard
,拖动卡片此时会从1 -> 0
,由于isDdragging
不会重新判断,所以,显示效果会是:
哪个卡片占原拖动卡片的位置,就会是有透明度
但这并不是我们所要的,所以,是否能拖动,使用唯一标识id
来判断。
还要注意,Kanban
中需要传入方法给Card
。
1
2
3
4
5
6
7
8
9
10
11
//...
this . state . cards . map ( ( card , index ) => {
return (
//...
< Card
//...
changeCard = { this . changeCard }
/> )
//...
})
//...
最后
真正使用时,还是需要定义DropTarget
中的drop
方法的,而文中只谈到了hover
方法,这部分让大家自己去探索吧。
打完收功。
后记
其实,一个插件能不能用,除了了解其用法之外,还可能需要了解其性能。
就本人的5年前的小笔记本来说,200个普通文本卡片还是可以自由拖动的,效果还是可以的。
当到300时,部分快速拖动会失效,可能是排序和渲染上的耗时吧,
好点的机器可能会好点。
另外,从调试过程看出,dnd
只会对有数据变化的卡片重新调用render()
渲染方法,并不是所有都会重新渲染一遍的。