<!DOCTYPE html>
<html>
<head>
  <link href='//fonts.googleapis.com/css?family=RobotoDraft:regular,bold,italic,thin,light,bolditalic,black,medium&amp;lang=en' rel='stylesheet' type='text/css'>
  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <h1>Sortable Grid Using GreenSock <a href="http://greensock.com/draggable" target="_blank">Draggable</a></h1>

  <div class="layout">
    <label><input id="mixed" name="layout" type="radio" value="mixed" checked=""/> Mixed Tile Size</label>
    <label><input id="fixed" name="layout" type="radio" value="fixed"/> Fixed Tile Size</label>
    <label><input id="column" name="layout" type="radio" value="column"/> Single Column</label>
  </div>
  
  <button id="add">ADD TILE</button>
  
  <div id="list"></div>
  
  
  <!-- VENDOR -->
  <script src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/TweenMax.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/latest/utils/Draggable.min.js"></script>
  
  <!-- APP -->
  <script src="script.js"></script>
  
</body>
</html>
$(function() {
  
  // GRID OPTIONS
  var rowSize   = 100;
  var colSize   = 100;
  var gutter    = 7;     // Spacing between tiles
  var numTiles  = 25;    // Number of tiles to initially populate the grid with
  var fixedSize = false; // When true, each tile's colspan will be fixed to 1
  var oneColumn = false; // When true, grid will only have 1 column and tiles have fixed colspan of 1
  var threshold = "50%"; // This is amount of overlap between tiles needed to detect a collision
  
  var $add  = $("#add");
  var $list = $("#list");
  var $mode = $("input[name='layout']");
  
  // Live node list of tiles
  var tiles  = $list[0].getElementsByClassName("tile");
  var label  = 1;
  var zIndex = 1000;
  
  var startWidth  = "100%";
  var startSize   = colSize;
  var singleWidth = colSize * 3;
  
  var colCount   = null;
  var rowCount   = null;
  var gutterStep = null;
  
  var shadow1 = "0 1px 3px  0 rgba(0, 0, 0, 0.5), 0 1px 2px 0 rgba(0, 0, 0, 0.6)";
  var shadow2 = "0 6px 10px 0 rgba(0, 0, 0, 0.3), 0 2px 2px 0 rgba(0, 0, 0, 0.2)";
  
  $(window).resize(resize);
  $add.click(createTile);
  $mode.change(init);
  
  init();
  
  // ========================================================================
  //  INIT
  // ========================================================================
  function init() {
  
    var width = startWidth;
  
    // This value is defined when this function 
    // is fired by a radio button change event
    switch (this.value) {
  
      case "mixed":
        fixedSize = false;
        oneColumn = false;
        colSize   = startSize;
        break;
  
      case "fixed":
        fixedSize = true;
        oneColumn = false;
        colSize   = startSize;
        break;
  
      case "column":
        fixedSize = false;
        oneColumn = true;
        width     = singleWidth;
        colSize   = singleWidth;
        break;
    }
  
    $(".tile").remove();
  
    TweenLite.to($list, 0.2, { width: width });
    TweenLite.delayedCall(0.25, populateBoard);
  
    function populateBoard() {
  
      label = 1;
      resize();
  
      for (var i = 0; i < numTiles; i++) {
        createTile();
      }
    }
  }
  
  
  // ========================================================================
  //  RESIZE
  // ========================================================================
  function resize() {
  
    colCount   = oneColumn ? 1 : Math.floor($list.outerWidth() / (colSize + gutter));
    gutterStep = colCount == 1 ? gutter : (gutter * (colCount - 1) / colCount);
    rowCount   = 0;
  
    layoutInvalidated();
  }
  
  
  // ========================================================================
  //  CHANGE POSITION
  // ========================================================================
  function changePosition(from, to, rowToUpdate) {
  
    var $tiles = $(".tile");
    var insert = from > to ? "insertBefore" : "insertAfter";
  
    // Change DOM positions
    $tiles.eq(from)[insert]($tiles.eq(to));
  
    layoutInvalidated(rowToUpdate);
  }
  
  // ========================================================================
  //  CREATE TILE
  // ========================================================================
  function createTile() {
  
    var colspan = fixedSize || oneColumn ? 1 : Math.floor(Math.random() * 2) + 1;
    var element = $("<div></div>").addClass("tile").html(label++);
    var lastX   = 0;
  
    Draggable.create(element, {
      onDrag      : onDrag,
      onPress     : onPress,
      onRelease   : onRelease,
      zIndexBoost : false
    });
  
    // NOTE: Leave rowspan set to 1 because this demo 
    // doesn't calculate different row heights
    var tile = {
      col        : null,
      colspan    : colspan,
      element    : element,
      height     : 0,
      inBounds   : true,
      index      : null,
      isDragging : false,
      lastIndex  : null,
      newTile    : true,
      positioned : false,
      row        : null,
      rowspan    : 1, 
      width      : 0,
      x          : 0,
      y          : 0
    };
  
    // Add tile properties to our element for quick lookup
    element[0].tile = tile;
  
    $list.append(element);
    layoutInvalidated();
  
    function onPress() {
  
      lastX = this.x;
      tile.isDragging = true;
      tile.lastIndex  = tile.index;
  
      TweenLite.to(element, 0.2, {
        autoAlpha : 0.75,
        boxShadow : shadow2,
        scale     : 0.95,
        zIndex    : "+=1000"
      });
    }
  
    function onDrag() {
  
      // Move to end of list if not in bounds
      if (!this.hitTest($list, 0)) {
        tile.inBounds = false;
        changePosition(tile.index, tiles.length - 1);
        return;
      }
  
      tile.inBounds = true;
  
      for (var i = 0; i < tiles.length; i++) {
  
        // Row to update is used for a partial layout update
        // Shift left/right checks if the tile is being dragged 
        // towards the the tile it is testing
        var testTile    = tiles[i].tile;
        var onSameRow   = (tile.row === testTile.row);
        var rowToUpdate = onSameRow ? tile.row : -1;
        var shiftLeft   = onSameRow ? (this.x < lastX && tile.index > i) : true;
        var shiftRight  = onSameRow ? (this.x > lastX && tile.index < i) : true;
        var validMove   = (testTile.positioned && (shiftLeft || shiftRight));
  
        if (this.hitTest(tiles[i], threshold) && validMove) {
          changePosition(tile.index, i, rowToUpdate);
          break;
        }
      }
  
      lastX = this.x;
    }
  
    function onRelease() {
  
      // Move tile back to last position if released out of bounds
      this.hitTest($list, 0)
        ? layoutInvalidated()
        : changePosition(tile.index, tile.lastIndex);
  
      TweenLite.to(element, 0.2, {
        autoAlpha : 1,
        boxShadow: shadow1,
        scale     : 1,
        x         : tile.x,
        y         : tile.y,
        zIndex    : ++zIndex
      });
  
      tile.isDragging = false;
    }
  }
  
  // ========================================================================
  //  LAYOUT INVALIDATED
  // ========================================================================
  function layoutInvalidated(rowToUpdate) {
  
    var timeline = new TimelineMax();
    var partialLayout = (rowToUpdate > -1);
  
    var height = 0;
    var col    = 0;
    var row    = 0;
    var time   = 0.35;
  
    $(".tile").each(function(index, element) {
  
      var tile    = this.tile;
      var oldRow  = tile.row;
      var oldCol  = tile.col;
      var newTile = tile.newTile;
  
      // PARTIAL LAYOUT: This condition can only occur while a tile is being 
      // dragged. The purpose of this is to only swap positions within a row, 
      // which will prevent a tile from jumping to another row if a space
      // is available. Without this, a large tile in column 0 may appear 
      // to be stuck if hit by a smaller tile, and if there is space in the 
      // row above for the smaller tile. When the user stops dragging the 
      // tile, a full layout update will happen, allowing tiles to move to
      // available spaces in rows above them.
      if (partialLayout) {
        row = tile.row;
        if (tile.row !== rowToUpdate) return;
      }
  
      // Update trackers when colCount is exceeded 
      if (col + tile.colspan > colCount) {
        col = 0; row++;
      }
  
      $.extend(tile, {
        col    : col,
        row    : row,
        index  : index,
        x      : col * gutterStep + (col * colSize),
        y      : row * gutterStep + (row * rowSize),
        width  : tile.colspan * colSize + ((tile.colspan - 1) * gutterStep),
        height : tile.rowspan * rowSize
      });
  
      col += tile.colspan;
  
      // If the tile being dragged is in bounds, set a new
      // last index in case it goes out of bounds
      if (tile.isDragging && tile.inBounds) {
        tile.lastIndex = index;
      }
  
      if (newTile) {
  
        // Clear the new tile flag
        tile.newTile = false;
  
        var from = {
          autoAlpha : 0,
          boxShadow : shadow1,
          height    : tile.height,
          scale     : 0,
          width     : tile.width
        };
  
        var to = {
          autoAlpha : 1,
          scale     : 1,
          zIndex    : zIndex
        }
  
        timeline.fromTo(element, time, from, to, "reflow");
      }
  
      // Don't animate the tile that is being dragged and
      // only animate the tiles that have changes
      if (!tile.isDragging && (oldRow !== tile.row || oldCol !== tile.col)) {
  
        var duration = newTile ? 0 : time;
  
        // Boost the z-index for tiles that will travel over 
        // another tile due to a row change
        if (oldRow !== tile.row) {
          timeline.set(element, { zIndex: ++zIndex }, "reflow");
        }
  
        timeline.to(element, duration, {
          x : tile.x,
          y : tile.y,
          onComplete : function() { tile.positioned = true; },
          onStart    : function() { tile.positioned = false; }
        }, "reflow");
      }
    });
  
    // If the row count has changed, change the height of the container
    if (row !== rowCount) {
      rowCount = row;
      height   = rowCount * gutterStep + (++row * rowSize);
      timeline.to($list, 0.2, { height: height }, "reflow");
    }
  }
});



html {
  font-family: RobotoDraft, 'Helvetica Neue', Helvetica, Arial;
}

body {
  background: #3A3A4D;
  padding: 25px;
}

*, *:before, *:after {
  box-sizing: border-box;
}

h1 {
  color: #efefef;
  font-weight: normal;
  font-weight: 300;
  margin-bottom: 30px;
}

h1 a {
  color: #efefef;
  border-bottom: 2px solid #efefef;
  text-decoration: none;
  padding-bottom: 3px;
}

h1 a:hover {
  color: yellowgreen;
  border-color: yellowgreen;
}

button {
  padding: 12px 30px;
  box-shadow: none;
  border: none;
  color: #efefef;
  cursor: pointer;
  background-color: rgba(0, 0, 0, 0.3);
}

button:hover {
  background-color: rgba(0, 0, 0, 0.35);
  color: yellowgreen;
}

button:focus {
  outline: none;
}

#list {
  background-color: rgba(0, 0, 0, 0.2);
  width: 250px;
  width: 90%;
  width: 0;
  position: relative;
  display: block;
  margin-top: 50px;
}

.tile {
  display: block;
  position: absolute;
  background-color: yellowgreen;
  color: #3A3A4D;
  padding: 5px;
  font-weight: bold;
}

.layout {
  color: #efefef;
  margin-bottom: 15px;
}

.layout label {
  margin-right: 15px;
  cursor: pointer;
}

.layout label:hover {
  color: yellowgreen;
}

.layout input {
  margin-right: 3px;
}


Demonstrates how to create an animated, sortable grid without using jQuery UI.