Drag & Drop Grid Layout in React
React implementation of a sortable drag and drop list organised into columns.
Made with
Html
Css/SCSS
Javascript
Html
<h1 class="title">Drag & Drop Grid Layout in React</h1>
<div id="react-root"></div>
Css
html {
width: 100%;
height: 100%;
background: radial-gradient(ellipse at top, #e66465, transparent),
radial-gradient(ellipse at bottom, #4d9f0c, transparent);
}
.title {
margin-top: 1.25em;
color: #eee;
text-align: center;
text-shadow: 1px 1px 0 black;
}
.items {
padding: 21px;
}
.item {
width: calc((100% / 3) - 26px);
height: 90px;
padding: 2.125em 0 1.25em;
user-select: none;
position: absolute;
border-radius: 3px;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2);
background: rgba(255, 255, 255, 0.75);
transition: background-color 0.1s ease-in-out;
cursor: grab;
text-align: center;
&.is-active {
background: rgba(255, 255, 255, 0.9);
cursor: grabbing;
}
}
Javascript
const dataStructure = [ // structure that models our initial rendered view of items
[0, 1, 2],
[3, 4, 5, 6, 7],
[8, 9, 10, 11]
]
const reinsert = (array, colFrom, rowFrom, colTo, rowTo) => {
const _array = array.slice(0);
const val = _array[colFrom][rowFrom];
_array[colFrom].splice(rowFrom, 1);
_array[colTo].splice(rowTo, 0, val);
calculateVisiblePositions(_array);
return _array;
}
const gutterPadding = 21;
const clamp = (n, min, max) => Math.max(Math.min(n, max), min);
const getColumnWidth = () => (window.innerWidth / dataStructure.length) - (gutterPadding / dataStructure.length); // spread columns over available window width
const height = 110; // crappy fixed item height :(
let width = getColumnWidth(),
layout = null;
// items are ordered by their index in this visual positions array
const calculateVisiblePositions = (newOrder) => {
width = getColumnWidth();
layout = newOrder.map((column, col) => {
return _.range(column.length + 1).map((item, row) => {
return [width * col, height * row];
});
});
}
// define spring motion opts
const springSetting1 = {stiffness: 180, damping: 10};
const springSetting2 = {stiffness: 150, damping: 16};
const List = React.createClass({
getInitialState() {
return {
mouse: [0, 0],
delta: [0, 0], // difference between mouse and item position, for dragging
lastPress: null, // key of the last pressed component
currentColumn: null,
isPressed: false,
order: dataStructure, // index: visual position. value: component key/id
isResizing: false
};
},
componentWillMount() {
this.resizeTimeout = null;
calculateVisiblePositions(dataStructure);
},
componentDidMount() {
window.addEventListener('touchmove', this.handleTouchMove);
window.addEventListener('mousemove', this.handleMouseMove);
window.addEventListener('touchend', this.handleMouseUp);
window.addEventListener('mouseup', this.handleMouseUp);
window.addEventListener('resize', this.handleResize);
},
componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
},
handleTouchStart(key, currentColumn, pressLocation, e) {
this.handleMouseDown(key, currentColumn, pressLocation, e.touches[0]);
},
handleTouchMove(e) {
e.preventDefault();
this.handleMouseMove(e.touches[0]);
},
handleMouseMove({pageX, pageY}) {
const {order, lastPress, currentColumn: colFrom, isPressed, delta: [dx, dy]} = this.state;
if (isPressed) {
const mouse = [pageX - dx, pageY - dy];
const colTo = clamp(Math.floor((mouse[0] + (width / 2)) / width), 0, 2);
const rowTo = clamp(Math.floor((mouse[1] + (height / 2)) / height), 0, 100);
const rowFrom = order[colFrom].indexOf(lastPress);
const newOrder = reinsert(order, colFrom, rowFrom, colTo, rowTo);
this.setState({
mouse,
order: newOrder,
currentColumn: colTo
});
}
},
handleMouseDown(key, currentColumn, [pressX, pressY], {pageX, pageY}) {
this.setState({
lastPress: key,
currentColumn,
isPressed: true,
delta: [pageX - pressX, pageY - pressY],
mouse: [pressX, pressY],
});
},
handleMouseUp() {
this.setState({
isPressed: false,
delta: [0, 0]
});
},
handleResize() {
clearTimeout(this.resizeTimeout);
this.applyResizingState(true);
// resize one last time after resizing stops, as sometimes this can be a little janky sometimes...
this.resizeTimeout = setTimeout(() => this.applyResizingState(false), 100);
},
applyResizingState(isResizing) {
this.setState({ isResizing });
calculateVisiblePositions(dataStructure);
},
render() {
const { order, lastPress, currentColumn, isPressed, mouse, isResizing } = this.state;
return (
<div className="items">
{order.map( (column, colIndex) => {
return (
column.map( (row) => {
let style,
x,
y,
visualPosition = order[colIndex].indexOf(row),
isActive = (row === lastPress && colIndex === currentColumn && isPressed);
if(isActive) {
[x, y] = mouse;
style = {
translateX: x,
translateY: y,
scale: ReactMotion.spring(1.1, springSetting1)
};
} else if(isResizing) {
[x, y] = layout[colIndex][visualPosition];
style = {
translateX: x,
translateY: y,
scale: 1
};
} else {
[x, y] = layout[colIndex][visualPosition];
style = {
translateX: ReactMotion.spring(x, springSetting2),
translateY: ReactMotion.spring(y, springSetting2),
scale: ReactMotion.spring(1, springSetting1)
};
}
return (
<ReactMotion.Motion key={row} style={style}>
{({translateX, translateY, scale}) =>
<div
onMouseDown={this.handleMouseDown.bind(null, row, colIndex, [x, y])}
onTouchStart={this.handleTouchStart.bind(null, row, colIndex, [x, y])}
className={isActive ? 'item is-active' : 'item'}
style={{
WebkitTransform: `translate3d(${translateX}px, ${translateY}px, 0) scale(${scale})`,
transform: `translate3d(${translateX}px, ${translateY}px, 0) scale(${scale})`,
zIndex: (row === lastPress && colIndex === currentColumn) ? 99 : visualPosition,
}}>Item {row + 1}</div>
}
</ReactMotion.Motion>
)
})
)
})}
</div>
)
}
});
ReactDOM.render(<List />, document.getElementById('react-root'));
Author
Tim Ramage
Demo
See the Pen Drag & Drop Grid Layout in React by Tim Ramage (@tjramage) on CodePen.