<!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>&nbsp;
                {{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;
  });