
<!DOCTYPE html>
<html lang='en' class=''>

<head>

  <meta charset='UTF-8'>
  <title>CodePen Demo</title>

  <meta name="robots" content="noindex">

  <link rel="shortcut icon" type="image/x-icon" href="https://cpwebassets.codepen.io/assets/favicon/favicon-aec34940fbc1a6e787974dcd360f2c6b63348d4b1f4e06c77743096d55480f33.ico">
  <link rel="mask-icon" href="https://cpwebassets.codepen.io/assets/favicon/logo-pin-b4b4269c16397ad2f0f7a01bcdf513a1994f4c94b8af2f191c09eb0d601762b1.svg" color="#111">
  <link rel="canonical" href="https://codepen.io/tjramage/pen/yOEbyw">

  <style>
*,
*:before,
*:after {
	box-sizing: border-box;
}
</style>
  
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">

  <style id="INLINE_PEN_STYLESHEET_ID">
    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;
  -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);
  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>

  
<script src="https://cpwebassets.codepen.io/assets/common/stopExecutionOnTimeout-2c7831bb44f98c1391d6a4ffda0e1fd302503391ca806e7fcc7b9b87197aec26.js"></script>
<script src="https://cpwebassets.codepen.io/assets/editor/iframe/iframeConsoleRunner-6d8bf8b4b479137260842506acbb12717dace0823c023e08b96360e60b0840d9.js"></script>
<script src="https://cpwebassets.codepen.io/assets/editor/iframe/iframeRefreshCSS-44fe83e49b63affec96918c9af88c0d80b209a862cf87ac46bc933074b8c557d.js"></script>
<script src="https://cpwebassets.codepen.io/assets/editor/iframe/iframeRuntimeErrors-4f205f2c14e769b448bcf477de2938c681660d5038bc464e3700256713ebe261.js"></script>
</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 &amp; Drop Grid Layout in React</h1>
<div id="react-root"></div>

  <script language="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({ displayName: "List",
  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 /*#__PURE__*/(
      React.createElement("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 /*#__PURE__*/(
              React.createElement(ReactMotion.Motion, { key: row, style: style },
              ({ translateX, translateY, scale }) => /*#__PURE__*/
              React.createElement("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)));



          }));

      })));


  } });


ReactDOM.render( /*#__PURE__*/React.createElement(List, null), document.getElementById('react-root'));



</script>

<script src="ddjs/react-with-addons-15.0.1.min.js"></script>
<script src="ddjs/react-dom-15.0.1.min.js"></script>
<script src="ddjs/react-motion.js"></script>
<script src="ddjs/lodash.min.js"></script>
  <script  src="https://cdpn.io/cpe/boomboom/pen.js?key=pen.js-06c22340-542c-7056-7a64-4495c4fd8218" crossorigin></script>
</body>



</html>