Skip to content
This repository was archived by the owner on Jul 11, 2023. It is now read-only.

Commit 741ff4a

Browse files
committed
Use React-DND to make a drag and drop interface for sorting category
priorities
1 parent 3c9c04f commit 741ff4a

File tree

8 files changed

+5826
-37
lines changed

8 files changed

+5826
-37
lines changed

.eslintrc.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
},
66
"rules": {
77
"indent": ["error", 4],
8-
"semi": ["error", "always"]
8+
"semi": ["error", "always"],
9+
"new-cap": 0
910
}
1011
}

config/gulp.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const webpack = require('webpack-stream');
77
const path = require('path');
88

99
const babelConfig = {
10-
presets: ['react', 'es2015'],
10+
presets: ['react', 'es2015', 'stage-0'],
1111
plugins: [
1212
[
1313
'css-modules-transform', {
@@ -76,6 +76,7 @@ module.exports = {
7676
module: {
7777
loaders: [{
7878
test: /\.js$/,
79+
exclude: /node_modules/,
7980
loader: 'babel-loader',
8081
query: {
8182
presets: ['react', 'es2015']
@@ -99,6 +100,7 @@ module.exports = {
99100
module: {
100101
loaders: [{
101102
test: /\.js$/,
103+
exclude: /node_modules/,
102104
loader: 'babel-loader',
103105
query: {
104106
presets: ['react', 'es2015']
Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,82 @@
11
import React, { PropTypes, Component } from 'react';
2+
import { DragDropContext } from 'react-dnd';
3+
import MultiBackend, { Preview } from 'react-dnd-multi-backend';
4+
import HTML5toTouch from 'react-dnd-multi-backend/lib/HTML5toTouch';
5+
import update from 'immutability-helper';
26

37
import CategoryListItem from './../organisms/CategoryListItem';
8+
import { CategoryListItemNameStatic } from './../organisms/CategoryListItemName';
49

510
class CategoryList extends Component {
611

7-
render() {
12+
constructor(props) {
13+
super(props);
14+
this.moveCard = this.moveCard.bind(this);
15+
this.state = {
16+
categories: []
17+
};
18+
}
819

20+
generatePreview(type, item, style) {
21+
const category = this.state.categories.find(category => {
22+
return category.id === item.id;
23+
});
924
return (
10-
<div className="category-list">
11-
{
12-
this.props.categories.map((categoryItem, index) => {
13-
return (<CategoryListItem
14-
key={index}
15-
name={categoryItem.name}
16-
icon={categoryItem.icon}
17-
rank={categoryItem.rank} />);
18-
})
19-
}
20-
</div>
21-
);
25+
<div style={style} className="category-list-item-dnd">
26+
<div className="category-list-item">
27+
<CategoryListItemNameStatic
28+
name={category.name}
29+
icon={category.icon} />
30+
</div>
31+
</div>
32+
);
33+
}
34+
35+
componentWillReceiveProps(newProps) {
36+
this.setState({
37+
categories: newProps.categories
38+
});
39+
}
2240

41+
moveCard(dragIndex, hoverIndex) {
42+
const { categories } = this.state;
43+
const dragCard = categories[dragIndex];
44+
45+
this.setState(update(this.state, {
46+
categories: {
47+
$splice: [
48+
[dragIndex, 1],
49+
[hoverIndex, 0, dragCard]
50+
]
51+
}
52+
}));
2353
}
2454

25-
}
55+
render() {
56+
return (
57+
<div className="category-list">
58+
<Preview generator={this.generatePreview.bind(this)} />
59+
{
60+
this.state.categories.map((categoryItem, index) => {
61+
return (
62+
<CategoryListItem
63+
key={categoryItem.id}
64+
index={index}
65+
id={categoryItem.id}
66+
name={categoryItem.name}
67+
icon={categoryItem.icon}
68+
moveCard={this.moveCard} />
69+
);
70+
})
71+
}
72+
</div>
73+
);
74+
}}
75+
76+
2677

2778
CategoryList.propTypes = {
2879
categories: PropTypes.array
2980
};
3081

31-
export default CategoryList;
82+
export default DragDropContext(MultiBackend(HTML5toTouch))(CategoryList);
Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
11
import React, { PropTypes, Component } from 'react';
22

3-
import Icon from './../atoms/Icon';
3+
import { CategoryListItemName } from './../organisms/CategoryListItemName';
44

55
class CategoryListItem extends Component {
6-
7-
constructor(props) {
8-
super(props);
9-
}
10-
116
render() {
127
return (
138
<div className="category-list-item">
14-
<div className="rank">
15-
<label>{this.props.rank}</label>
16-
</div>
17-
18-
<div className="category-name card">
19-
{
20-
this.props.icon &&
21-
<Icon className="medium padding">{this.props.icon}</Icon>
22-
}
23-
<span className="category-name-label">{this.props.name}</span>
24-
</div>
25-
</div>
9+
{
10+
this.props.index + 1 &&
11+
<div className="rank">
12+
<label>{this.props.index + 1}</label>
13+
</div>
14+
}
15+
16+
<CategoryListItemName
17+
index={this.props.index}
18+
id={this.props.id}
19+
name={this.props.name}
20+
icon={this.props.icon}
21+
moveCard={this.props.moveCard} />
22+
</div>
2623
);
2724
}
28-
2925
}
3026

3127
CategoryListItem.propTypes = {
3228
name: PropTypes.string,
3329
icon: PropTypes.string,
34-
rank: PropTypes.number
30+
index: PropTypes.number,
31+
moveCard: PropTypes.func,
32+
id: PropTypes.number
3533
};
3634

3735
export default CategoryListItem;
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
'use strict';
2+
3+
import React, { Component, PropTypes } from 'react';
4+
import Icon from './../atoms/Icon';
5+
import { findDOMNode } from 'react-dom';
6+
import { DragSource, DropTarget } from 'react-dnd';
7+
8+
const ItemTypes = {
9+
CARD: 'card'
10+
};
11+
12+
const cardSource = {
13+
beginDrag(props) {
14+
return {
15+
id: props.id,
16+
index: props.index
17+
};
18+
}
19+
};
20+
21+
const cardTarget = {
22+
hover(props, monitor, component) {
23+
const dragIndex = monitor.getItem().index;
24+
const hoverIndex = props.index;
25+
26+
// Don't replace items with themselves
27+
if (dragIndex === hoverIndex) {
28+
return;
29+
}
30+
31+
// Determine rectangle on screen
32+
const hoverBoundingRect = findDOMNode(component).getBoundingClientRect();
33+
34+
// Get vertical middle
35+
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
36+
37+
// Determine mouse position
38+
const clientOffset = monitor.getClientOffset();
39+
40+
// Get pixels to the top
41+
const hoverClientY = clientOffset.y - hoverBoundingRect.top;
42+
43+
// Only perform the move when the mouse has crossed half of the items height
44+
// When dragging downwards, only move when the cursor is below 50%
45+
// When dragging upwards, only move when the cursor is above 50%
46+
47+
// Dragging downwards
48+
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
49+
return;
50+
}
51+
52+
// Dragging upwards
53+
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
54+
return;
55+
}
56+
57+
// Time to actually perform the action
58+
props.moveCard(dragIndex, hoverIndex);
59+
60+
// Note: we're mutating the monitor item here!
61+
// Generally it's better to avoid mutations,
62+
// but it's good here for the sake of performance
63+
// to avoid expensive index searches.
64+
monitor.getItem().index = hoverIndex;
65+
}
66+
}
67+
68+
class CategoryListItemName extends Component {
69+
render() {
70+
const {
71+
connectDragSource,
72+
connectDropTarget,
73+
isDragging
74+
} = this.props;
75+
76+
const template = (
77+
<div className={`category-name card ${isDragging ? 'hidden' : ''}`}>
78+
{
79+
this.props.icon &&
80+
<Icon className="medium padding">{this.props.icon}</Icon>
81+
}
82+
<span className="category-name-label">{this.props.name}</span>
83+
</div>
84+
);
85+
86+
if (connectDragSource && connectDropTarget) {
87+
return connectDragSource(connectDropTarget((template)));
88+
}
89+
return template;
90+
}
91+
}
92+
93+
CategoryListItemName.propTypes = {
94+
icon: PropTypes.string,
95+
name: PropTypes.string,
96+
index: PropTypes.number,
97+
connectDragSource: PropTypes.func,
98+
connectDropTarget: PropTypes.func,
99+
isDragging: PropTypes.bool
100+
};
101+
102+
const DropCategoryListItemName = DropTarget(ItemTypes.CARD, cardTarget, (connect) => ({
103+
connectDropTarget: connect.dropTarget()
104+
}))(CategoryListItemName);
105+
106+
const DragDropCategoryListItemName = DragSource(ItemTypes.CARD, cardSource, (connect, monitor) => ({
107+
connectDragSource: connect.dragSource(),
108+
isDragging: monitor.isDragging()
109+
}))(DropCategoryListItemName);
110+
111+
export {
112+
DragDropCategoryListItemName as CategoryListItemName,
113+
CategoryListItemName as CategoryListItemNameStatic
114+
};
115+

frontend/styles/category-list-item.scss

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
align-items: center;
2020
margin-bottom: 0;
2121

22+
&.hidden {
23+
opacity: 0;
24+
}
25+
2226
&.card {
2327
padding: 1em 2em;
2428
.icon {
@@ -30,4 +34,14 @@
3034
font-size: 1.125em;
3135
}
3236
}
37+
}
38+
39+
.category-list-item-dnd {
40+
width: 100%;
41+
padding: 0 1em 0 5em;
42+
margin-left: -5em;
43+
44+
.category-list-item {
45+
margin: 0em;
46+
}
3347
}

0 commit comments

Comments
 (0)