<!DOCTYPE html>
<html ng-app="ubSortable">

  <head>
    <script data-require="angular.js@*" data-semver="1.2.9" src="http://code.angularjs.org/1.2.9/angular.js"></script>
    <script data-require="jquery@*" data-semver="2.0.3" src="http://code.jquery.com/jquery-2.0.3.min.js"></script>
    <link rel="stylesheet" href="style.css" />
    <script src="script.js"></script>
  </head>

  <body ng-controller="sortCtrl">
    <h1>Sort</h1>
    
    <p>Every item depends on it's divisors.</p>
    
    <p>You should not be able to position an item to be above its dependencies (or below items that depend on it)</p>
    
    <div ub-sortable="obj.items" ub-sortable-depends="depends" >
      <div ng-repeat="i in obj.items track by i.title" ub-sortable-item="i" class="listItem" style="background-color: {{i.color}}">
        <h3>{{i.title}}</h3>
        Depends On: {{ dependsOn(i).join(", ") }}
        <a class='removeItem' ng-click="removeItem($index)">&times;</a>
        <div class="drag-handle" ub-drag></div>
      </div>
    </div>
    
    <button ng-click="addItem()">Add Item</button>
    
    <pre>{{obj.items | json }}</pre>
    
  </body>

</html>
(function() {
  var sortable = angular.module("ubSortable", []);
  
  // Test controller
  sortable.controller("sortCtrl", function($scope) {
    $scope.obj = {
      items: []
    };
    
    var i = 1;
    
    $scope.depends = function(a, b) {
      if(b.num >= a.num) return false;
      return a.num % b.num === 0;
    };

    var randomColor = function() {
      return 'rgb(' + (Math.floor((256-239)*Math.random()) + 210) + ',' + 
                      (Math.floor((256-239)*Math.random()) + 210) + ',' + 
                      (Math.floor((256-239)*Math.random()) + 210) + ')';
    };
    
    $scope.addItem = function() {
      $scope.obj.items.push({
        title: "Item #" + i,
        num: i,
        color: randomColor()
      });
      i++;
    };
    
    $scope.dependsOn = function(item) {
      var result = [];
      for(var i = 0; i < $scope.obj.items.length;i++) {
        if($scope.depends(item, $scope.obj.items[i])) {
          result.push($scope.obj.items[i].title);
        }
      }
      return result;
    };
    
    $scope.addItem();
    $scope.addItem();
    $scope.addItem();
    $scope.addItem();
    $scope.addItem();
    $scope.addItem();
    $scope.addItem();

    $scope.removeItem = function(index) {
      $scope.obj.items.splice(index, 1);
    };
    
  });

  // Use the ub-sortable attribute on the container of the sort
  // The ub-sortable-depends attribute should be a function(a,b) -> boolean
  // where 'a' and 'b' are items of the list and the function should return
  // whether 'a' depends on 'b'
  sortable.directive("ubSortable", function() {
    return {
      restrict: "A",
      scope: {
        list: "=ubSortable",
        dependsFunc: "=?ubSortableDepends",
        horizontal: "@"
      },
      controller: function($scope){
        $scope.dependsFunc = $scope.dependsFunc || function() { return false };

        this.list = $scope.list;
        this.depends = $scope.dependsFunc;
        this.horizontal = !angular.isUndefined($scope.horizontal);

        this.getSortLimits = function(item) {
          var result = {
            up: [],
            down: [],
          };
          var list = $scope.list;
          var index = list.indexOf(item);
          if(index == -1) {
            return result;
          }
          var i;
          for(i = index - 1;i >= 0;i--) {
            if($scope.dependsFunc(list[index], list[i]) || !list[i].$$ubSortBounds) break;
            result.up.push({index: i, pos: list[i].$$ubSortBounds().bottom});
          }
          for(i = index + 1;i < list.length;i++) {
            if($scope.dependsFunc(list[i], list[index]) || !list[i].$$ubSortBounds) break;
            result.down.push({index: i, pos: list[i].$$ubSortBounds().top});
          }
          
          return result;
        };
        
        this.moveItem = function(item, newPos) {
          $scope.$apply(function() {
            var list = $scope.list;
            var pos = list.indexOf(item);
            list.splice(newPos, 0, list.splice(pos, 1)[0]);
          });
        };
      }
    };
  });
  
  // Every item that should be sortable should be given the ub-sortable-item 
  // attribute, with it's value being the item of the array which we are sorting. 
  sortable.directive("ubSortableItem", function() {
    return {
      restrict: "A",
      require: "^ubSortable",
      scope: {
        item: "=ubSortableItem"
      },
      link: function(scope, element, attributes, sortableCtrl) {
        scope.list = sortableCtrl.list;
        scope.depends = sortableCtrl.depends;
        scope.horizontal = sortableCtrl.horizontal;
        
        // scope.ubSortableItem.$$ubSortBounds = function() {
        //     var offset = $(element).offset();
        //     var height = $(element).outerHeight();
        //     var middle = offset.top + (height / 2);
             
        //     var result = {top: middle, bottom: middle};
        //     return result;
        // };
        // scope.SortLimits = function() {
        //   return sortableCtrl.getSortLimits(scope.ubSortableItem);
        // };
        // scope.SortMove = function(pos) {
        //   sortableCtrl.moveItem(scope.ubSortableItem, pos);
        // };
      },
      controller: function($scope, $attrs, $element) {
        var setDragStyles = function(limits) {
          if(limits.up.length === 0)
            $attrs.$addClass("ub-drag-no-up");
          else
            $attrs.$removeClass("ub-drag-no-up");
          if(limits.down.length === 0)
            $attrs.$addClass("ub-drag-no-down");
          else
            $attrs.$removeClass("ub-drag-no-down");
        };
        
        var sortLimits = function() {
          var result = {up: [], down: []};
          
          $($element).prevAll().each(function() {
            var scope = angular.element(this).isolateScope();
            if(angular.isUndefined(scope) || angular.isUndefined(scope.item)) return false;
            if(scope.depends($scope.item, scope.item)) return false;
            var offset = $(this).offset();
            result.up.push({
              pos: $scope.list.indexOf(scope.item),
              top: offset.top,
              left: offset.left,
              height: $(this).outerHeight(),
              width: $(this).outerWidth()
            });
          });
          
          $($element).nextAll().each(function() {
            var scope = angular.element(this).isolateScope();
            if(angular.isUndefined(scope) || angular.isUndefined(scope.item)) return false;
            if(scope.depends(scope.item, $scope.item)) return false;
            var offset = $(this).offset();
            result.down.push({
              pos: $scope.list.indexOf(scope.item),
              top: offset.top,
              left: offset.left,
              height: $(this).outerHeight(),
              width: $(this).outerWidth()
            });
          });
          
          return result;
        };
        
        this.mouseDown = function(e) {
          $attrs.$addClass("ub-drag");
          $($element).css({
            position: "relative",
            zIndex: 10
          });
          
          var posIndex = "pageY";
          var posCss = "top";
          var posFunc = function(i) {
            return i.top + (i.height / 2);
          }
          if($scope.horizontal) {
            posIndex = "pageX";
            posCss = "left";
            posFunc = function(i) {
              return i.left + (i.width / 2);
            };
          }
          
          var start = e[posIndex];
          var limits = sortLimits();
          //setDragStyles(limits);

          var mouseMove = function(e) {
            var pos = e[posIndex];
            var cssPos = pos - start;

            var swapPos = -1;
            var i;
            
            if(limits.up.length > 0) {
              for(i = 0;i < limits.up.length;i++) {
                if(pos < posFunc(limits.up[i])) {
                  swapPos = limits.up[i].pos;
                }
              }
            } else {
              cssPos = Math.max(0, cssPos);
            }
            if(limits.down.length > 0) {
              for(i = 0;i < limits.down.length;i++) {
                if(pos > posFunc(limits.down[i])) {
                  swapPos = limits.down[i].pos;
                }
              }
            } else {
              cssPos = Math.min(0, cssPos);
            }
            
            $($element).css(posCss, cssPos);

            if(swapPos != -1) {
              $scope.$apply(function() {
                var list = $scope.list;
                var pos = list.indexOf($scope.item);
                list.splice(swapPos, 0, list.splice(pos, 1)[0]);
              });
              limits = sortLimits();
              start = e[posIndex];
              setDragStyles(limits);
            }
          };
          var mouseUp = function(e) {
            $attrs.$removeClass("ub-drag");
            $attrs.$removeClass("ub-drag-no-up");
            $attrs.$removeClass("ub-drag-no-down");
            $($element).css({
              position: "",
              zIndex: "",
              top: "",
            });
            $(document).off("mousemove", mouseMove);
          };
          $(document).on("mousemove", mouseMove);
          $(document).one("mouseup", mouseUp);
        };
      }
    };
  });
  
  // This directive should be placed on the HTML element which should initiate
  // dragging
  sortable.directive("ubDrag", function() {
    var selStart = "onselectstart" in document.createElement( "div" );
    
    return {
      restrict: "A",
      require: "^ubSortableItem",
      link: function(scope, element, attributes, sortableCtrl) {
        // Prevent text selection while dragging
        if(selStart) {
          $(element).on("selectstart", false);
        }
        $(element).mousedown(function(e) {
          e.preventDefault();
          return sortableCtrl.mouseDown(e);
        });
      }
    };
  });
})();
/* Styles go here */

body > div {
  padding: 8px;
  border: 1px solid black;
  border-radius: 5px;
  width: 450px;
}

.listItem {
  height: 80px;
  padding: 5px;
  position: relative;
  border-radius: 5px;
}

[ub-drag] {
  cursor: ns-resize;
}

.drag-handle {
  width: 32px;
  height: 32px;
  position: absolute;
  right: 5px;
  top: 30px;
  background: -moz-linear-gradient(top,  rgba(160,160,160,0) 0%, rgba(160,160,160,0) 17%, rgba(160,160,160,1) 18%, rgba(160,160,160,1) 32%, rgba(160,160,160,0) 33%, rgba(160,160,160,0) 42%, rgba(160,160,160,1) 43%, rgba(160,160,160,1) 57%, rgba(160,160,160,0) 58%, rgba(160,160,160,0) 67%, rgba(160,160,160,1) 68%, rgba(160,160,160,1) 82%, rgba(160,160,160,0) 83%, rgba(160,160,160,0) 100%); /* FF3.6+ */
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(160,160,160,0)), color-stop(17%,rgba(160,160,160,0)), color-stop(18%,rgba(160,160,160,1)), color-stop(32%,rgba(160,160,160,1)), color-stop(33%,rgba(160,160,160,0)), color-stop(42%,rgba(160,160,160,0)), color-stop(43%,rgba(160,160,160,1)), color-stop(57%,rgba(160,160,160,1)), color-stop(58%,rgba(160,160,160,0)), color-stop(67%,rgba(160,160,160,0)), color-stop(68%,rgba(160,160,160,1)), color-stop(82%,rgba(160,160,160,1)), color-stop(83%,rgba(160,160,160,0)), color-stop(100%,rgba(160,160,160,0))); /* Chrome,Safari4+ */
  background: -webkit-linear-gradient(top,  rgba(160,160,160,0) 0%,rgba(160,160,160,0) 17%,rgba(160,160,160,1) 18%,rgba(160,160,160,1) 32%,rgba(160,160,160,0) 33%,rgba(160,160,160,0) 42%,rgba(160,160,160,1) 43%,rgba(160,160,160,1) 57%,rgba(160,160,160,0) 58%,rgba(160,160,160,0) 67%,rgba(160,160,160,1) 68%,rgba(160,160,160,1) 82%,rgba(160,160,160,0) 83%,rgba(160,160,160,0) 100%); /* Chrome10+,Safari5.1+ */
  background: -o-linear-gradient(top,  rgba(160,160,160,0) 0%,rgba(160,160,160,0) 17%,rgba(160,160,160,1) 18%,rgba(160,160,160,1) 32%,rgba(160,160,160,0) 33%,rgba(160,160,160,0) 42%,rgba(160,160,160,1) 43%,rgba(160,160,160,1) 57%,rgba(160,160,160,0) 58%,rgba(160,160,160,0) 67%,rgba(160,160,160,1) 68%,rgba(160,160,160,1) 82%,rgba(160,160,160,0) 83%,rgba(160,160,160,0) 100%); /* Opera 11.10+ */
  background: -ms-linear-gradient(top,  rgba(160,160,160,0) 0%,rgba(160,160,160,0) 17%,rgba(160,160,160,1) 18%,rgba(160,160,160,1) 32%,rgba(160,160,160,0) 33%,rgba(160,160,160,0) 42%,rgba(160,160,160,1) 43%,rgba(160,160,160,1) 57%,rgba(160,160,160,0) 58%,rgba(160,160,160,0) 67%,rgba(160,160,160,1) 68%,rgba(160,160,160,1) 82%,rgba(160,160,160,0) 83%,rgba(160,160,160,0) 100%); /* IE10+ */
  background: linear-gradient(to bottom,  rgba(160,160,160,0) 0%,rgba(160,160,160,0) 17%,rgba(160,160,160,1) 18%,rgba(160,160,160,1) 32%,rgba(160,160,160,0) 33%,rgba(160,160,160,0) 42%,rgba(160,160,160,1) 43%,rgba(160,160,160,1) 57%,rgba(160,160,160,0) 58%,rgba(160,160,160,0) 67%,rgba(160,160,160,1) 68%,rgba(160,160,160,1) 82%,rgba(160,160,160,0) 83%,rgba(160,160,160,0) 100%); /* W3C */
  filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00a0a0a0', endColorstr='#00a0a0a0',GradientType=0 ); /* IE6-9 */
}

.ub-drag {
  box-shadow: 1px 1px 3px black;
  -ms-transform: translate(-2px,-2px);
  -o-transform: translate(-2px,-2px);
  -moz-transform: translate(-2px,-2px);
  -webkit-transform: translate(-2px,-2px);
  transform: translate(-2px,-2px);
}

.ub-drag-no-up {
  box-shadow: 1px 1px 3px black, 0 -2px 0px 0px red;
}

.ub-drag-no-down {
  box-shadow: 0 2px 0px 0px red, 1px 1px 3px black;
}

.ub-drag-no-up.ub-drag-no-down {
  box-shadow: 0 -2px 0px 0px red, 0 2px 0px 0px red, 1px 1px 3px black;
}

a.removeItem {
  color: red;
  font-weight: bold;
  background-color: white;
  border-radius: 50px;
  width: 15px;
  height: 15px;
  display: block;
  position:absolute;
  right: 5px;
  top: 5px;
  text-align: center;
  line-height: 15px;
  cursor: pointer;
}

a.removeItem:hover {
  background-color: red;
  color: white;
}

Sortable Test
=============

Test for an angular implementation of a sortable list where certain items can 
depend on others.