<!DOCTYPE html>
<html>
<head>
<link data-require="bootstrap-css@*" data-semver="3.3.1" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" />
<link rel="stylesheet" href="style.css" />
<script data-require="angular.js@*" data-semver="1.3.15" src="https://code.angularjs.org/1.3.15/angular.js"></script>
<script data-require="angular-ui-bootstrap@*" data-semver="0.12.0" src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.12.0.min.js"></script>
<script src="https://rawgit.com/marceljuenemann/angular-drag-and-drop-lists/master/angular-drag-and-drop-lists.js"></script>
<script src="script.js"></script>
<script src="models.js"></script>
<script src="utils.js"></script>
</head>
<body ng-app="dndTreeTest">
<div class="jumbotron">
<div class="container">
<h1>Tree</h1>
<h2>Using <a href="https://github.com/marceljuenemann/angular-drag-and-drop-lists">angular-drag-drop-list</a></h2>
<p>Baskets and boxes can stay in the root level of the tree, basket can contain only boxes, boxes can contain only fruits.</p>
<small>Example by <a href="https://kuus.github.io">kuus</a></small>
</div>
</div>
<div class="container" ng-controller="TreeCtrl">
<div class="row">
<div class="col-sm-6">
<h4>Build the tree</h4>
<ol class="tree" dnd-list="tree" dnd-allowed-types="['basket','box']" dnd-drop="helperDnd.drop(event, index, item, tree)">
<li ng-repeat="model in tree" dnd-draggable="model" dnd-type="model.subject" dnd-effect-allowed="copyMove" dnd-dragstart="helperDnd.dragstart(model, tree, $index)" dnd-moved="helperDnd.moved(model, tree, $index)" dnd-copied="duplicate(model, tree, $index)" class="tree-component" ng-include="'tree-' + model.subject + '.html'"></li>
</ol>
<script type="text/ng-template" id="tree-basket.html">
<ol class="tree-basket"
ng-init="boxes=model.boxes"
dnd-list="boxes"
dnd-allowed-types="['box']"
dnd-drop="helperDnd.drop(event, index-1, item, boxes)"
dnd-dragover="helperDnd.dragover(event, index)">
<li ng-include="'tree-component-header.html'" ng-init="list=tree"></li>
<li ng-repeat="model in boxes"
dnd-draggable="model"
dnd-type="'box'"
dnd-effect-allowed="copyMove"
dnd-dragstart="helperDnd.dragstart(model, boxes, $index)"
dnd-moved="helperDnd.moved(model, boxes, $index)"
dnd-copied="duplicate(model, boxes, $index)"
class="tree-component"
ng-include="'tree-box.html'">
</li>
</ol>
</script>
<script type="text/ng-template" id="tree-box.html">
<ol class="tree-box"
ng-init="fruits=model.fruits"
dnd-list="fruits"
dnd-allowed-types="['fruit']"
dnd-drop="helperDnd.drop(event, index-1, item, fruits)"
dnd-dragover="helperDnd.dragover(event, index)">
<li ng-include="'tree-component-header.html'" ng-init="list=boxes||tree"></li>
<li ng-repeat="model in fruits"
dnd-draggable="model"
dnd-type="'fruit'"
dnd-effect-allowed="copyMove"
dnd-dragstart="helperDnd.dragstart(model, fruits, $index)"
dnd-moved="helperDnd.moved(model, fruits, $index)"
dnd-copied="duplicate(model, fruits, $index)"
class="tree-component"
ng-include="'tree-fruit.html'">
</li>
</ol>
</script>
<script type="text/ng-template" id="tree-fruit.html">
<div class="tree-fruit">
<div ng-include="'tree-component-header.html'" ng-init="list=fruits"></div>
</div>
</script>
<script type="text/ng-template" id="tree-component-header.html">
<div class="tree-subject tree-subject-{{model.subject}}">
<div class="pull-right btn-group btn-group-actions">
<a class="btn btn-xs glyphicon glyphicon-plus" ng-click="newChild(model)" ng-if="model.subject!=='fruit'" tooltip="{{ model.subject==='basket' ? 'Add basket' : 'Add box'}}"></a>
<a class="btn btn-xs text-danger glyphicon glyphicon-remove" ng-click="removeMe(this, model, list)" tooltip="Remove"></a>
</div>
<h3>
<span class="text-success" ng-switch="model.subject">
<i ng-switch-when="basket" class="glyphicon glyphicon-trash"></i>
<i ng-switch-when="box" class="glyphicon glyphicon-inbox"></i>
<i ng-switch-when="fruit" class="glyphicon glyphicon-tags"></i>
</span>
{{model.subject|uppercase}}: <small>{{model.id}}</small>
</h3>
</div>
</script>
<ol class="tree">
<li class="tree-component" ng-click="addBasket()">
<div class="tree-subject tree-action">
<h3>Add basket</h3>
</div>
</li>
<li class="tree-component" ng-click="addBox()">
<div class="tree-subject tree-action">
<h3>Add box</h3>
</div>
</li>
</ol>
</div>
<div class="col-sm-6">
<h4>Tree model to json</h4>
<pre class="pre-scrollable">{{ tree|json }}</pre>
</div>
</div>
</div>
</body>
</html>
/* global angular */
'use strict';
angular.module('dndTreeTest', ['ui.bootstrap', 'dndLists'])
/**
* Helper Drag and drop (`dnd` related service)
*
*/
.service('HelperDnd', function (Utils) {
var isDraggingFrom = 0;
var draggingOriginList = [];
return {
dragstart: function (model, list, index) {
console.log('drag', model.id, index);
draggingOriginList = list;
isDraggingFrom = index;
},
moved: function (model, list, index) {
console.log('moved', model.id, index);
list.splice(index, 1);
return model;
},
/**
* Drop (`dnd` related method)
*
* @param {[type]} event [description]
* @param {[type]} index [description]
* @param {[type]} model [description]
* @param {[type]} list [description]
* @return {[type]} [description]
*/
drop: function (event, index, model, list) {
// @see https://github.com/marceljuenemann/angular-drag-and-drop-lists/issues/54
if (index < 0) {
return;
}
var isDraggingTo = index;
var result = Utils.findById(list, model.id);
// if we are dropping the model in the same list, just reordering it
if (result) {
// first remove the model
Utils.removeByID(list, model.id);
// if it has been moved down from the first position fix the dnd bug
// @see https://github.com/marceljuenemann/angular-drag-and-drop-lists/issues/54
if (isDraggingTo > isDraggingFrom && isDraggingFrom === 0) {
isDraggingTo--;
}
// insert the model in the new position
list.splice(isDraggingTo, 0, model);
console.log('dropped down', model.id, isDraggingTo);
// if the list array is still empty or the model is coming from another list
} else {
// first remove the model
if (angular.isArray(draggingOriginList)) {
Utils.removeByID(draggingOriginList, model.id);
}
// if the destination list is not empty insert the model at the right position
if (list.length) {
list.splice(isDraggingTo, 0, model);
// if the destination list is empty just push it
} else {
list.push(model);
}
}
// console.log('drop', index, model, list);
// return model;
},
dragover: function(event, index) {
return index > 0;
}
};
})
.controller('TreeCtrl', function ($scope, ModelsManager, Utils, HelperDnd) {
$scope.tree = [];
/**
* Add basket on root level of the tree
*
* @return {void}
*/
$scope.addBasket = function () {
var basket = ModelsManager.addBasket();
$scope.tree.push(basket);
};
/**
* Add box on root level of the tree
*
* @return {void}
*/
$scope.addBox = function () {
var box = ModelsManager.addBox();
$scope.tree.push(box);
};
/**
* Helper drag and drop
* just expose all the service method on the scope.
*
* @type {Service}
*/
$scope.helperDnd = HelperDnd;
/**
* Remove customize component
*
* @param {Object} scope The ui-tree scope.
* @param {Object} model The customize component model to delete.
* @return {void}
*/
$scope.removeMe = function (scope, model, list) {
Utils.removeByID(list, model.id);
};
/**
* Collapse all models in the tree.
*
* @return {void}
*/
$scope.collapseAll = function () {
$scope.$broadcast('collapseAll');
};
/**
* Expand all customize components in the tree, using the ui-tree API.
*
* @return {void}
*/
$scope.expandAll = function () {
$scope.$broadcast('expandAll');
};
/**
* Move up or down a specific customize component
*
* @param {Object} model The model to move.
* @param {array} list Array in which the model is contained.
* @param {string} upOrDown Either `'up'` or `'down'`.
* @return {void}
*/
$scope.move = function (model, list, upOrDown) {
Utils.moveByID(upOrDown, list, model.id);
};
/**
* Add child to the given model
*
* @param {Object} model
* @return {void}
*/
$scope.newChild = function (model) {
var newModel;
if (model.subject === 'basket') {
newModel = ModelsManager.addBox(null, { 'basket_id': model.id });
model.boxes.push(newModel);
} else if (model.subject === 'box') {
newModel = ModelsManager.addFruit();
//model.__proto__.addFruit(newModel);
model.fruits.push(newModel);
}
// this.expand(); // ui-tree method
};
});
$tree--indent-width: 30px;
$tree--drag-border-width: 5px;
$tree--drag-bg: lightgreen;
$tree--drag-bg-accent: darken($tree--drag-bg, 30%);
.tree-component {
position: relative;
min-height: 45px;
}
.tree-subject {
position: relative;
border-bottom: 1px solid #dfdfdf;
background: #fff;
padding: 0 0 0 14px;
cursor: move;
&:hover {
background: #f5f5f5;
}
/**
* Block title
*/
> h3 {
height: 45px;
margin: 0;
padding: 14px 10px 14px 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
font-weight: bold;
}
/**
* Group of actions buttons (edit, duplicate, remove, etc.)
*/
> .btn-group-actions {
display: none;
margin: 10px 0 0 0;
}
&:hover > .btn-group-actions {
display: block;
}
}
/**
*
*/
.tree-action {
background: transparent;
cursor: pointer;
> .btn-expand {
color: #999;
float: left;
background: transparent;
}
&:hover > .btn-expand {
color: #666;
}
> h3 {
padding-left: 0;
font-weight: normal;
color: #999;
}
&:hover > h3 {
color: #666;
}
}
ol,
li {
padding: 0;
margin: 0;
list-style: none;
}
/**
* Style the dragged element while is being dragged
*/
.dndDragging {}
/**
* Hide the dragging source
*/
.dndDraggingSource {
display: none;
}
/**
* A border that's get colored during dnd
* (for plugin `angular-drag-and-drop-lists`)
*/
[dnd-list] {
border-left: $tree--drag-border-width solid transparent;
}
/**
* Make everything during the dragging more smooth,
* less flickering in the UI
* (for plugin `angular-drag-and-drop-lists`)
*/
[dnd-list],
.customize-subject {
transition: all .18s ease;
}
/**
* Style the dnd experience when dragging
* (for plugin `angular-drag-and-drop-lists`)
*/
.dndDragover {
/**
* Highlight the container when `drag-hovered`
*/
&[dnd-list] {
padding: 0 0 42px 0;
border-left: $tree--drag-border-width solid $tree--drag-bg-accent;
// background-color: $tree--drag-bg;
// background: linear-gradient($tree--drag-bg 99.4%, $tree--drag-bg-accent 100%);
}
/**
* and highlight also the container's component label when `drag-hovered`
*/
> li > .customize-subject {
background-color: darken($tree--drag-bg, 5%);
}
}
/**
* Dragged component placeholder
* (for plugin `angular-drag-and-drop-lists`)
*/
.dndPlaceholder {
background-color: desaturate($tree--drag-bg-accent, 60%);
min-height: 42px;
display: block;
position: relative;
margin-left: $tree--indent-width + $tree--drag-border-width;
.tree-section & {
margin-left: $tree--indent-width;
}
}
/**
* The tree root list
*/
.tree {
margin: 0;
background: #eee;
border-top: 1px solid #ccc;
> .dndPlaceholder {
margin-left: $tree--drag-border-width;
}
}
/**
* Manage the indentation of the tree
*/
.tree-basket .tree-box,
.tree-box .tree-fruit {
margin-left: $tree--indent-width;
}
#Angular drag and drop tree
Using [angular-drag-drop-list](https://github.com/marceljuenemann/angular-drag-and-drop-lists)
Baskets and boxes can stay in the root level of the tree, basket can contain only boxes, boxes can contain only fruits.
'use strict';
angular.module('dndTreeTest')
.service('Utils', function () {
return {
/**
* Get random id
*
* @link http://stackoverflow.com/a/1349426/1938970
* @param {int} length The length of the id to create.
* @return {String} The generated random id.
*/
getRandomId: function (length) {
var text = '';
var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-';
for ( var i=0; i < length; i++ )
text += possible.charAt(Math.floor(Math.random() * possible.length));
return text;
},
/**
* Find object by id in given array of objects, (each array
* element object must have an id property).
*
* @param {Array} array The array containing the object.
* @param {String} id The id of the object to find.
* @return {Object.<Object, int>|boolean} An object containg the found object and
* its position in the given array, or false
* if the object is not in the array.
*/
findById: function (array, id) {
var i = 0;
while (i < array.length) {
if (array[i].id === id) {
return {
obj: array[i],
pos: i
};
}
i++;
}
// throw new Error('Object with id ' + id + ' not found in array');
return false;
},
/**
* Remove object (by given id) from array.
*
* @param {Array} array The array containing the object to move.
* @param {String} objectId The id of the object to move.
* @return {Array} The cleaned array.
*/
removeByID: function (array, objectId) {
var result = this.findById(array, objectId);
if (!result) {
return;
}
var index = result.pos;
array.splice(index, 1);
return array;
}
};
});
/* global angular */
'use strict';
angular.module('dndTreeTest')
/**
* Models Manager
* @return {ModelsManager}
*/
.service('ModelsManager', function (Basket, Box, Fruit) {
return {
baskets: {},
boxes: {},
fruits: {},
/**
* Add basket
* @param {string} id [description]
* @param {Object} args [description]
*/
addBasket: function (id, args) {
var self = this;
var model;
if (id instanceof Basket) {
model = id;
} else {
model = new Basket(id, args);
}
this.baskets[model.id] = model;
return model;
},
/**
* Add box
* @param {string} id [description]
* @param {Object} args [description]
*/
addBox: function (id, args) {
var model;
if (id instanceof Box) {
model = id;
} else {
model = new Box(id, args);
}
this.boxes[model.id] = model;
return model;
},
/**
* Add fruit
* @param {string} id [description]
* @param {Object} args [description]
*/
addFruit: function (id, args) {
var model;
if (id instanceof Fruit) {
model = id;
} else {
model = new Fruit(id, args);
}
this.fruits[model.id] = model;
return model;
}
};
})
/**
* Basket
*
* @class
*/
.factory('Basket', function (Utils) {
/**
* @constructor
* @param {[type]} data [description]
*/
function Basket(id, args) {
this['id'] = id || Utils.getRandomId(7);
this.boxes = [];
if (args) {
this.setArgs(args);
}
this.subject = 'basket';
}
Basket.prototype = {
setArgs: function (args) {
angular.extend(this, args);
},
addBox: function (box) {
if (this.boxes.indexOf(box.id) === -1) {
this.boxes.push(box);
} else {
console.log('box is already in basket', this.id, box);
}
}
};
return Basket;
})
/**
* Box
*
* @class
*/
.factory('Box', function (Utils) {
/**
* @constructor
* @param {[type]} data [description]
*/
function Box(id, args) {
this['id'] = id || Utils.getRandomId(7);
this['basket_id'] = '';
this.fruits = [];
if (args) {
this.setArgs(args);
}
this.subject = 'box';
}
Box.prototype = {
setArgs: function (args) {
angular.extend(this, args);
},
addFruit: function (fruit) {
if (this.fruits.indexOf(fruit.id) === -1) {
this.fruits.push(fruit);
} else {
console.log('fruit is already in box', this.id, fruit);
}
}
};
return Box;
})
/**
* Fruit
*
* @class
*/
.factory('Fruit', function (Utils) {
/**
* @constructor
* @param {[type]} data [description]
*/
function Fruit(id, args) {
this['id'] = id || Utils.getRandomId(7);
this.subject = 'fruit';
}
Fruit.prototype = {
setArgs: function (args) {
angular.extend(this, args);
}
};
return Fruit;
});