draggable tree in AngularUI

by johnwun

HTML

<script src="http://code.jquery.com/ui/1.9.2/jquery-ui.js"></script>
<script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.3/angular.js"></script>
<script src="https://raw.github.com/furf/nestedSortable/master/jquery.mjs.nestedSortable.js"></script>
<link rel="stylesheet" href="http://code.jquery.com/ui/1.9.2/themes/base/jquery-ui.css">
<script src="http://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.2/jquery.ui.touch-punch.min.js"></script>
<div ng-app="app" ng-cloak>
  <div ng-controller="TreeController">
    
    <ol ui-nested-sortable="{
      listTypes: ['I','A',['1','a']],
      doNotClear: true,
      placeholder: 'ui-state-highlight',
      forcePlaceholderSize: true,
      toleranceElement: '> div'
    }">
      <li ng-repeat="child in data.children" tree>
          <div><strong>{{child.title}}</strong></div>
        <ol><branch></ol>
      </li>
    </ol>
            
  </div>
</div>

CSS

[ng-cloak] { display:none; }
body { font:12px helvetica,arial,sans-serif; }
ol {
    margin:0;
}
li {
    cursor:move;
    padding:0;
    margin:1px;
    background:#eee;
    border:1px solid #ddd;
    padding:0.25em;
}

JavaScript

angular.module('app', ['directives', 'controllers']);

angular.module('directives', [])

/**
 * tree
 */
.directive('tree', ['$compile', function ($compile) {

  return {
    restrict: 'A',
    compile: function (tElement, tAttrs) {

      var branch = tElement.find('branch'),
          repeatExpr,
          childExpr,
          childrenExpr;

      if (!branch.length) {
        throw new Error('tree directive must contain a branch node.');
      }

      repeatExpr = (branch.attr('branch') || tAttrs.ngRepeat).match(/^(.*) in (?:.*\.)?(.*)$/);
      childExpr = repeatExpr[1];
      childrenExpr = repeatExpr[2];
      tElement.attr('ng-repeat', childExpr + ' in ' + childExpr + '.' + childrenExpr);
      
      return function link (scope, element, attrs) {

        scope.$depth = scope.$depth || 0;
        scope.$watch(childExpr, function(child) {

          var childScope = scope.$new();

          childScope[childrenExpr] = child[childrenExpr];
          childScope.$depth = scope.$depth + 1;

          element.find('branch').replaceWith($compile(tElement.clone())(childScope));
        });
      };
    }
  };
}])

/**
 * uiNestedSortable
 */
.directive('uiNestedSortable', ['$parse', function($parse) {

    'use strict';

    var eventTypes = 'Create Start Sort Change BeforeStop Stop Update Receive Remove Over Out Activate Deactivate'.split(' ');

    return {
        restrict: 'A',
        link: function(scope, element, attrs) {

            var options = attrs.uiNestedSortable ? $parse(attrs.uiNestedSortable)() : {};

            angular.forEach(eventTypes, function(eventType) {

                var attr = attrs['uiNestedSortable' + eventType],
                    callback;

                if (attr) {
                    callback = $parse(attr);
                    options[eventType.toLowerCase()] = function(event, ui) {
                        scope.$apply(function() {

                            ui.hierarchy = element.nestedSortable('toHierarchy', {
        ...