<link href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css" rel="stylesheet" id="bootstrap-css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.0.0/js/bootstrap.min.js"></script>
<script src="//code.jquery.com/jquery-1.11.1.min.js"></script>
<!------ Include the above in your HEAD tag ---------->
<!DOCTYPE html><html class=''>
<head><script src='//production-assets.codepen.io/assets/editor/live/console_runner-079c09a0e3b9ff743e39ee2d5637b9216b3545af0de366d4b9aad9dc87e26bfd.js'></script><script src='//production-assets.codepen.io/assets/editor/live/events_runner-73716630c22bbc8cff4bd0f07b135f00a0bdc5d14629260c3ec49e5606f98fdd.js'></script><script src='//production-assets.codepen.io/assets/editor/live/css_live_reload_init-2c0dc5167d60a5af3ee189d570b1835129687ea2a61bee3513dee3a50c115a77.js'></script><meta charset='UTF-8'><meta name="robots" content="noindex"><link rel="shortcut icon" type="image/x-icon" href="//production-assets.codepen.io/assets/favicon/favicon-8ea04875e70c4b0bb41da869e81236e54394d63638a1ef12fa558a4a835f1164.ico" /><link rel="mask-icon" type="" href="//production-assets.codepen.io/assets/favicon/logo-pin-f2d2b6d2c61838f7e76325261b7195c27224080bc099486ddd6dccb469b8e8e6.svg" color="#111" /><link rel="canonical" href="https://codepen.io/tjramage/pen/yOEbyw?q=react&order=popularity&depth=everything&show_forks=false" />
<style>
*,
*:before,
*:after {
box-sizing: border-box;
}
</style>
<link rel='stylesheet prefetch' href='https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css'>
<style class="cp-pen-styles">html {
width: 100%;
height: 100%;
background: -webkit-radial-gradient(50% 16% circle, #219eb0 32%, #3f679d 88%);
background: radial-gradient(circle at 50% 16%, #219eb0 32%, #3f679d 88%);
}
.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;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
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);
-webkit-transition: background-color 0.1s ease-in-out;
transition: background-color 0.1s ease-in-out;
cursor: -webkit-grab;
cursor: grab;
text-align: center;
}
.item.is-active {
background: rgba(255, 255, 255, 0.9);
cursor: -webkit-grabbing;
cursor: grabbing;
}
</style></head><body>
<!--
This is a proof of concept based on Cheng Lou's
totally amazing React Motion library:
https://github.com/chenglou/react-motion
Really just a proof of concept to see if I could get
it working across multiple columns (i.e. no source order
reflowing) for a kanban / trello list app I'm working on.
However, I couldn't elegantly solve the issue of variable
item heights in calculateVisiblePositions(), so have decided
to drop this as it is and move over to react-dnd instead.
If anyone ever figures out how to do this with variable
item heights, please let me know!
-->
<h1 class="title">Drag & Drop Grid Layout in React</h1>
<div id="react-root"></div>
<script src='//production-assets.codepen.io/assets/common/stopExecutionOnTimeout-b2a7b3fe212eaa732349046d8416e00a9dec26eb7fd347590fbced3ab38af52e.js'></script><script src='//fb.me/react-with-addons-15.0.1.min.js'></script><script src='//fb.me/react-dom-15.0.1.min.js'></script><script src='//npmcdn.com/react-motion@0.4.2/build/react-motion.js'></script><script src='//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.6.1/lodash.min.js'></script>
<script >'use strict';
var dataStructure = [// structure that models our initial rendered view of items
[0, 1, 2], [3, 4, 5, 6, 7], [8, 9, 10, 11]];
var reinsert = function reinsert(array, colFrom, rowFrom, colTo, rowTo) {
var _array = array.slice(0);
var val = _array[colFrom][rowFrom];
_array[colFrom].splice(rowFrom, 1);
_array[colTo].splice(rowTo, 0, val);
calculateVisiblePositions(_array);
return _array;
};
var gutterPadding = 21;
var clamp = function clamp(n, min, max) {
return Math.max(Math.min(n, max), min);
};
var getColumnWidth = function getColumnWidth() {
return window.innerWidth / dataStructure.length - gutterPadding / dataStructure.length;
}; // spread columns over available window width
var height = 110; // crappy fixed item height :(
var width = getColumnWidth(),
layout = null;
// items are ordered by their index in this visual positions array
var calculateVisiblePositions = function calculateVisiblePositions(newOrder) {
width = getColumnWidth();
layout = newOrder.map(function (column, col) {
return _.range(column.length + 1).map(function (item, row) {
return [width * col, height * row];
});
});
};
// define spring motion opts
var springSetting1 = { stiffness: 180, damping: 10 };
var springSetting2 = { stiffness: 150, damping: 16 };
var List = React.createClass({
displayName: 'List',
getInitialState: function 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: function componentWillMount() {
this.resizeTimeout = null;
calculateVisiblePositions(dataStructure);
},
componentDidMount: function 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: function componentWillUnmount() {
window.removeEventListener('resize', this.handleResize);
},
handleTouchStart: function handleTouchStart(key, currentColumn, pressLocation, e) {
this.handleMouseDown(key, currentColumn, pressLocation, e.touches[0]);
},
handleTouchMove: function handleTouchMove(e) {
e.preventDefault();
this.handleMouseMove(e.touches[0]);
},
handleMouseMove: function handleMouseMove(_ref) {
var pageX = _ref.pageX;
var pageY = _ref.pageY;
var _state = this.state;
var order = _state.order;
var lastPress = _state.lastPress;
var colFrom = _state.currentColumn;
var isPressed = _state.isPressed;
var _state$delta = _state.delta;
var dx = _state$delta[0];
var dy = _state$delta[1];
if (isPressed) {
var mouse = [pageX - dx, pageY - dy];
var colTo = clamp(Math.floor((mouse[0] + width / 2) / width), 0, 2);
var rowTo = clamp(Math.floor((mouse[1] + height / 2) / height), 0, 100);
var rowFrom = order[colFrom].indexOf(lastPress);
var newOrder = reinsert(order, colFrom, rowFrom, colTo, rowTo);
this.setState({
mouse: mouse,
order: newOrder,
currentColumn: colTo
});
}
},
handleMouseDown: function handleMouseDown(key, currentColumn, _ref2, _ref3) {
var pressX = _ref2[0];
var pressY = _ref2[1];
var pageX = _ref3.pageX;
var pageY = _ref3.pageY;
this.setState({
lastPress: key,
currentColumn: currentColumn,
isPressed: true,
delta: [pageX - pressX, pageY - pressY],
mouse: [pressX, pressY]
});
},
handleMouseUp: function handleMouseUp() {
this.setState({
isPressed: false,
delta: [0, 0]
});
},
handleResize: function handleResize() {
var _this = this;
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(function () {
return _this.applyResizingState(false);
}, 100);
},
applyResizingState: function applyResizingState(isResizing) {
this.setState({ isResizing: isResizing });
calculateVisiblePositions(dataStructure);
},
render: function render() {
var _this2 = this;
var _state2 = this.state;
var order = _state2.order;
var lastPress = _state2.lastPress;
var currentColumn = _state2.currentColumn;
var isPressed = _state2.isPressed;
var mouse = _state2.mouse;
var isResizing = _state2.isResizing;
return React.createElement(
'div',
{ className: 'items' },
order.map(function (column, colIndex) {
return column.map(function (row) {
var style = undefined,
x = undefined,
y = undefined,
visualPosition = order[colIndex].indexOf(row),
isActive = row === lastPress && colIndex === currentColumn && isPressed;
if (isActive) {
x = mouse[0];
y = mouse[1];
style = {
translateX: x,
translateY: y,
scale: ReactMotion.spring(1.1, springSetting1)
};
} else if (isResizing) {
var _layout$colIndex$visu = layout[colIndex][visualPosition];
x = _layout$colIndex$visu[0];
y = _layout$colIndex$visu[1];
style = {
translateX: x,
translateY: y,
scale: 1
};
} else {
var _layout$colIndex$visu2 = layout[colIndex][visualPosition];
x = _layout$colIndex$visu2[0];
y = _layout$colIndex$visu2[1];
style = {
translateX: ReactMotion.spring(x, springSetting2),
translateY: ReactMotion.spring(y, springSetting2),
scale: ReactMotion.spring(1, springSetting1)
};
}
return React.createElement(
ReactMotion.Motion,
{ key: row, style: style },
function (_ref4) {
var translateX = _ref4.translateX;
var translateY = _ref4.translateY;
var scale = _ref4.scale;
return React.createElement(
'div',
{
onMouseDown: _this2.handleMouseDown.bind(null, row, colIndex, [x, y]),
onTouchStart: _this2.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
);
}
);
});
})
);
}
});
ReactDOM.render(React.createElement(List, null), document.getElementById('react-root'));
//# sourceURL=pen.js
</script>
</body></html>