1/**
2 * jQuery org-chart/tree plugin.
3 *
4 * Author: Wes Nolte
5 * http://twitter.com/wesnolte
6 *
7 * Based on the work of Mark Lee
8 * http://www.capricasoftware.co.uk
9 *
10 * Copyright (c) 2011 Wesley Nolte
11 * Dual licensed under the MIT and GPL licenses.
12 *
13 */
14(function($) {
15
16  $.fn.jOrgChart = function(options) {
17    var opts = $.extend({}, $.fn.jOrgChart.defaults, options);
18    var $appendTo = $(opts.chartElement);
19
20    // build the tree
21    $this = $(this);
22    var $container = $("<div class='" + opts.chartClass + "'/>");
23    if($this.is("ul")) {
24      buildNode($this.find("li:first"), $container, 0, opts);
25    }
26    else if($this.is("li")) {
27      buildNode($this, $container, 0, opts);
28    }
29    $appendTo.append($container);
30
31    // add drag and drop if enabled
32    if(opts.dragAndDrop){
33        $('div.node').draggable({
34            cursor      : 'move',
35            distance    : 40,
36            helper      : 'clone',
37            opacity     : 0.8,
38            revert      : 'invalid',
39            revertDuration : 100,
40            snap        : 'div.node.expanded',
41            snapMode    : 'inner',
42            stack       : 'div.node'
43        });
44
45        $('div.node').droppable({
46            accept      : '.node',
47            activeClass : 'drag-active',
48            hoverClass  : 'drop-hover'
49        });
50
51      // Drag start event handler for nodes
52      $('div.node').bind("dragstart", function handleDragStart( event, ui ){
53
54        var sourceNode = $(this);
55        sourceNode.parentsUntil('.node-container')
56                   .find('*')
57                   .filter('.node')
58                   .droppable('disable');
59      });
60
61      // Drag stop event handler for nodes
62      $('div.node').bind("dragstop", function handleDragStop( event, ui ){
63
64        /* reload the plugin */
65        $(opts.chartElement).children().remove();
66        $this.jOrgChart(opts);
67      });
68
69      // Drop event handler for nodes
70      $('div.node').bind("drop", function handleDropEvent( event, ui ) {
71
72        var targetID = $(this).data("tree-node");
73        var targetLi = $this.find("li").filter(function() { return $(this).data("tree-node") === targetID; } );
74        var targetUl = targetLi.children('ul');
75
76        var sourceID = ui.draggable.data("tree-node");
77        var sourceLi = $this.find("li").filter(function() { return $(this).data("tree-node") === sourceID; } );
78        var sourceUl = sourceLi.parent('ul');
79
80        if (targetUl.length > 0){
81          targetUl.append(sourceLi);
82        } else {
83          targetLi.append("<ul></ul>");
84          targetLi.children('ul').append(sourceLi);
85        }
86
87        //Removes any empty lists
88        if (sourceUl.children().length === 0){
89          sourceUl.remove();
90        }
91
92      }); // handleDropEvent
93
94    } // Drag and drop
95  };
96
97  // Option defaults
98  $.fn.jOrgChart.defaults = {
99    chartElement : 'body',
100    depth      : -1,
101    chartClass : "jOrgChart",
102    dragAndDrop: false
103  };
104
105  var nodeCount = 0;
106  // Method that recursively builds the tree
107  function buildNode($node, $appendTo, level, opts) {
108    var $table = $("<table cellpadding='0' cellspacing='0' border='0'/>");
109    var $tbody = $("<tbody/>");
110
111    // Construct the node container(s)
112    var $nodeRow = $("<tr/>").addClass("node-cells");
113    var $nodeCell = $("<td/>").addClass("node-cell").attr("colspan", 2);
114    var $childNodes = $node.children("ul:first").children("li");
115    var $nodeDiv;
116
117    if($childNodes.length > 1) {
118      $nodeCell.attr("colspan", $childNodes.length * 2);
119    }
120    // Draw the node
121    // Get the contents - any markup except li and ul allowed
122    var $nodeContent = $node.clone()
123                            .children("ul,li")
124                            .remove()
125                            .end()
126                            .html();
127
128      //Increaments the node count which is used to link the source list and the org chart
129    nodeCount++;
130    $node.data("tree-node", nodeCount);
131    $nodeDiv = $("<div>").addClass("node")
132                                     .data("tree-node", nodeCount)
133                                     .append($nodeContent);
134
135    // Expand and contract nodes
136    if ($childNodes.length > 0) {
137      $nodeDiv.click(function() {
138          var $this = $(this);
139          var $tr = $this.closest("tr");
140
141          if($tr.hasClass('contracted')){
142            $this.css('cursor','n-resize');
143            $tr.removeClass('contracted').addClass('expanded');
144            $tr.nextAll("tr").css('visibility', '');
145
146            // Update the <li> appropriately so that if the tree redraws collapsed/non-collapsed nodes
147            // maintain their appearance
148            $node.removeClass('collapsed');
149          }else{
150            $this.css('cursor','s-resize');
151            $tr.removeClass('expanded').addClass('contracted');
152            $tr.nextAll("tr").css('visibility', 'hidden');
153
154            $node.addClass('collapsed');
155          }
156        });
157    }
158
159    $nodeCell.append($nodeDiv);
160    $nodeRow.append($nodeCell);
161    $tbody.append($nodeRow);
162
163    if($childNodes.length > 0) {
164      // if it can be expanded then change the cursor
165      $nodeDiv.css('cursor','n-resize');
166
167      // recurse until leaves found (-1) or to the level specified
168      if(opts.depth == -1 || (level+1 < opts.depth)) {
169        var $downLineRow = $("<tr/>");
170        var $downLineCell = $("<td/>").attr("colspan", $childNodes.length*2);
171        $downLineRow.append($downLineCell);
172
173        // draw the connecting line from the parent node to the horizontal line
174        $downLine = $("<div></div>").addClass("line down");
175        $downLineCell.append($downLine);
176        $tbody.append($downLineRow);
177
178        // Draw the horizontal lines
179        var $linesRow = $("<tr/>");
180        $childNodes.each(function() {
181          var $left = $("<td>&nbsp;</td>").addClass("line left top");
182          var $right = $("<td>&nbsp;</td>").addClass("line right top");
183          $linesRow.append($left).append($right);
184        });
185
186        // horizontal line shouldn't extend beyond the first and last child branches
187        $linesRow.find("td:first")
188                    .removeClass("top")
189                 .end()
190                 .find("td:last")
191                    .removeClass("top");
192
193        $tbody.append($linesRow);
194        var $childNodesRow = $("<tr/>");
195        $childNodes.each(function() {
196           var $td = $("<td class='node-container'/>");
197           $td.attr("colspan", 2);
198           // recurse through children lists and items
199           buildNode($(this), $td, level+1, opts);
200           $childNodesRow.append($td);
201        });
202
203      }
204      $tbody.append($childNodesRow);
205    }
206
207    // any classes on the LI element get copied to the relevant node in the tree
208    // apart from the special 'collapsed' class, which collapses the sub-tree at this point
209    if ($node.attr('class') != undefined) {
210        var classList = $node.attr('class').split(/\s+/);
211        $.each(classList, function(index,item) {
212            if (item == 'collapsed') {
213                $nodeRow.nextAll('tr').css('visibility', 'hidden');
214                    $nodeRow.removeClass('expanded');
215                    $nodeRow.addClass('contracted');
216                    $nodeDiv.css('cursor','s-resize');
217            } else {
218                $nodeDiv.addClass(item);
219            }
220        });
221    }
222
223    $table.append($tbody);
224    $appendTo.append($table);
225
226    /* Prevent trees collapsing if a link inside a node is clicked */
227    $nodeDiv.children('a').click(function(e){
228        e.stopPropagation();
229    });
230  };
231
232})(jQuery);
233