React-dnd Intro-2

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 { childrenconnectDropTarget } = 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

通过最后处绑定,Cardprops会多出来个值来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;
  }
};

因为,当一个拖动卡片的index1时,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()渲染方法,并不是所有都会重新渲染一遍的。

Comments