<!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)">×</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.