JSFiddle - React, Tailwind, and code Playground

by chodorowicz

HTML

<script src="http://cdnjs.cloudflare.com/ajax/libs/jqueryui-touch-punch/0.2.2/jquery.ui.touch-punch.min.js"></script>
<script src="https://raw.github.com/mjsarfatti/nestedSortable/master/jquery.mjs.nestedSortable.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/angular.js/1.1.1/angular.min.js"></script>
<div ng-app="app" ng-cloak>
  <div ng-controller="TreeController">
    <div>
      <button ng-click="addChild(data)">+ New</button>
    </div>
    <div class="live">
      <ol ui-nested-sortable="{
        listType: 'ol',
        items: 'li',
        doNotClear: true,
        placeholder: 'ui-state-highlight',
        forcePlaceholderSize: true,
        toleranceElement: '> div'
      }" ui-nested-sortable-stop="update($event, $ui)">
        <li ya-tree="child in data.children at ol" ng-class="{minimized:child.minimized}">
          <div>
            <button class="toggle" ng-click="toggleMinimized(child)" ng-switch on="child.minimized"><span ng-switch-when="true">&#x25B6;</span><span ng-switch-default>&#x25BC;</span></button>
            <input ng-model="child.title" />
            <button ng-click="addChild(child)">+</button>
            <button ng-click="remove(child)">x</button>
          </div>
          <ol ng-class="{pregnant:child.children.length}"></ol>
        </li>
      </ol>
    </div>
    <div class="shadow">
      <ol>
        <li ya-tree="child in data.children at ol" class="bg{{$depth%6}}" ng-class="{minimized:child.minimized}">
          <div>
            <input disabled value="{{child.title}}" /> <em>({{$depth}})</em>
          </div>
          <ol ng-class="{pregnant:child.children.length}"></ol>
        </li>
      </ol>
    </div>
  </div>
</div>

CSS

[ng-cloak] {
  display: none;
}
* {
  box-sizing: border-box
}
body {
  font: 12px/16px helvetica, arial, sans-serif;
  background: #eee;
}
ol {
  margin:4px 0;
  border: 0 none transparent;
  padding: 0;
  background:#eee;
}
li {
  list-style:none;
  margin: 1px;
  border: 1px solid #ddd;
  padding: 8px 8px 4px 8px;
  background: #fff;
}
.ui-sortable li {
  cursor: move;
}
.live input, .shadow input {
  margin: 0;
  border: 1px solid #ddd;
  padding: 1px;
  font: inherit;
  font-weight: bold;
  height:2em;
}
.ui-state-highlight {
  border: 1px solid #fc0;
  background: #ffe;
}
.live, .shadow {
  float: left;
  width: 45%;
  margin-right: 5%;
}
.shadow {
  opacity: 0.7;
}
.debug {
  clear: both;
}
.debug textarea {
  width: 100%;
  height: 40em;
  font: 10px/13px monospace;
}
textarea, input {
  outline: none;
}
.pregnant {
  border: 1px solid #ddd;
}
.bg0 { background:#fcc; }
.bg1 { background:#ffc; }
.bg2 { background:#cfc; }
.bg3 { background:#cff; }
.bg4 { background:#ccf; }
.bg5 { background:#fcf; }
.minimized > ol > li { display:none; }
.minimized > ol {   border: 0 none transparent; }
.toggle { border: 0 none transparent; background:transparent; width:2em; color:#aaa; }
button { cursor: pointer }

JavaScript

var app = angular.module('app', []);

app.directive('yaTree', function () {

  return {
    restrict: 'A',
    transclude: 'element',
    priority: 1000,
    terminal: true,
    compile: function (tElement, tAttrs, transclude) {

      var repeatExpr, childExpr, rootExpr, childrenExpr;

      repeatExpr = tAttrs.yaTree.match(/^(.*) in ((?:.*\.)?(.*)) at (.*)$/);
      childExpr = repeatExpr[1];
      rootExpr = repeatExpr[2];
      childrenExpr = repeatExpr[3];
      branchExpr = repeatExpr[4];

      return function link (scope, element, attrs) {

        var rootElement = element[0].parentNode,
            cache = [];

        // Reverse lookup object to avoid re-rendering elements
        function lookup (child) {
          var i = cache.length;
          while (i--) {
            if (cache[i].scope[childExpr] === child) {
              return cache.splice(i, 1)[0];
            }
          }
        }

        scope.$watch(rootExpr, function (root) {

          var currentCache = [];

          // Recurse the data structure
          (function walk (children, parentNode, parentScope, depth) {

            var i = 0,
                n = children.length,
                last = n - 1,
                cursor,
                child,
                cached,
                childScope,
                grandchildren;

            // Iterate the children at the current level
            for (; i < n; ++i) {

              // We will compare the cached element to the element in 
              // at the destination index. If it does not match, then 
              // the cached element is being moved into this position.
              cursor = parentNode.childNodes[i];

              child = children[i];

              // See if this child has been previously rendered
              // using a reverse lookup by object reference
              cached = lookup(child);
              
              // If the parentScope no longer matches, we've moved.
              // We'll have...