1/*
2 * jQuery treetable Plugin 3.1.0
3 * http://ludo.cubicphuse.nl/jquery-treetable
4 *
5 * Copyright 2013, Ludo van den Boom
6 * Dual licensed under the MIT or GPL Version 2 licenses.
7 */
8(function() {
9  var $, Node, Tree, methods;
10
11  $ = jQuery;
12
13  Node = (function() {
14    function Node(row, tree, settings) {
15      var parentId;
16
17      this.row = row;
18      this.tree = tree;
19      this.settings = settings;
20
21      // TODO Ensure id/parentId is always a string (not int)
22      this.id = this.row.data(this.settings.nodeIdAttr);
23
24      // TODO Move this to a setParentId function?
25      parentId = this.row.data(this.settings.parentIdAttr);
26      if (parentId != null && parentId !== "") {
27        this.parentId = parentId;
28      }
29
30      this.treeCell = $(this.row.children(this.settings.columnElType)[this.settings.column]);
31      this.expander = $(this.settings.expanderTemplate);
32      this.indenter = $(this.settings.indenterTemplate);
33      this.children = [];
34      this.initialized = false;
35      this.treeCell.prepend(this.indenter);
36    }
37
38    Node.prototype.addChild = function(child) {
39      return this.children.push(child);
40    };
41
42    Node.prototype.ancestors = function() {
43      var ancestors, node;
44      node = this;
45      ancestors = [];
46      while (node = node.parentNode()) {
47        ancestors.push(node);
48      }
49      return ancestors;
50    };
51
52    Node.prototype.collapse = function() {
53      if (this.collapsed()) {
54        return this;
55      }
56
57      this.row.removeClass("expanded").addClass("collapsed");
58
59      this._hideChildren();
60      this.expander.attr("title", this.settings.stringExpand);
61
62      if (this.initialized && this.settings.onNodeCollapse != null) {
63        this.settings.onNodeCollapse.apply(this);
64      }
65
66      return this;
67    };
68
69    Node.prototype.collapsed = function() {
70      return this.row.hasClass("collapsed");
71    };
72
73    // TODO destroy: remove event handlers, expander, indenter, etc.
74
75    Node.prototype.expand = function() {
76      if (this.expanded()) {
77        return this;
78      }
79
80      this.row.removeClass("collapsed").addClass("expanded");
81
82      if (this.initialized && this.settings.onNodeExpand != null) {
83        this.settings.onNodeExpand.apply(this);
84      }
85
86      if ($(this.row).is(":visible")) {
87        this._showChildren();
88      }
89
90      this.expander.attr("title", this.settings.stringCollapse);
91
92      return this;
93    };
94
95    Node.prototype.expanded = function() {
96      return this.row.hasClass("expanded");
97    };
98
99    Node.prototype.hide = function() {
100      this._hideChildren();
101      this.row.hide();
102      return this;
103    };
104
105    Node.prototype.isBranchNode = function() {
106      if(this.children.length > 0 || this.row.data(this.settings.branchAttr) === true) {
107        return true;
108      } else {
109        return false;
110      }
111    };
112
113    Node.prototype.updateBranchLeafClass = function(){
114      this.row.removeClass('branch');
115      this.row.removeClass('leaf');
116      this.row.addClass(this.isBranchNode() ? 'branch' : 'leaf');
117    };
118
119    Node.prototype.level = function() {
120      return this.ancestors().length;
121    };
122
123    Node.prototype.parentNode = function() {
124      if (this.parentId != null) {
125        return this.tree[this.parentId];
126      } else {
127        return null;
128      }
129    };
130
131    Node.prototype.removeChild = function(child) {
132      var i = $.inArray(child, this.children);
133      return this.children.splice(i, 1)
134    };
135
136    Node.prototype.render = function() {
137      var handler,
138          settings = this.settings,
139          target;
140
141      if (settings.expandable === true && this.isBranchNode()) {
142        handler = function(e) {
143          $(this).parents("table").treetable("node", $(this).parents("tr").data(settings.nodeIdAttr)).toggle();
144          return e.preventDefault();
145        };
146
147        this.indenter.html(this.expander);
148        target = settings.clickableNodeNames === true ? this.treeCell : this.expander;
149
150        target.off("click.treetable").on("click.treetable", handler);
151        target.off("keydown.treetable").on("keydown.treetable", function(e) {
152          if (e.keyCode == 13) {
153            handler.apply(this, [e]);
154          }
155        });
156      }
157
158      this.indenter[0].style.paddingLeft = "" + (this.level() * settings.indent) + "px";
159
160      return this;
161    };
162
163    Node.prototype.reveal = function() {
164      if (this.parentId != null) {
165        this.parentNode().reveal();
166      }
167      return this.expand();
168    };
169
170    Node.prototype.setParent = function(node) {
171      if (this.parentId != null) {
172        this.tree[this.parentId].removeChild(this);
173      }
174      this.parentId = node.id;
175      this.row.data(this.settings.parentIdAttr, node.id);
176      return node.addChild(this);
177    };
178
179    Node.prototype.show = function() {
180      if (!this.initialized) {
181        this._initialize();
182      }
183      this.row.show();
184      if (this.expanded()) {
185        this._showChildren();
186      }
187      return this;
188    };
189
190    Node.prototype.toggle = function() {
191      if (this.expanded()) {
192        this.collapse();
193      } else {
194        this.expand();
195      }
196      return this;
197    };
198
199    Node.prototype._hideChildren = function() {
200      var child, _i, _len, _ref, _results;
201      _ref = this.children;
202      _results = [];
203      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
204        child = _ref[_i];
205        _results.push(child.hide());
206      }
207      return _results;
208    };
209
210    Node.prototype._initialize = function() {
211      var settings = this.settings;
212
213      this.render();
214
215      if (settings.expandable === true && settings.initialState === "collapsed") {
216        this.collapse();
217      } else {
218        this.expand();
219      }
220
221      if (settings.onNodeInitialized != null) {
222        settings.onNodeInitialized.apply(this);
223      }
224
225      return this.initialized = true;
226    };
227
228    Node.prototype._showChildren = function() {
229      var child, _i, _len, _ref, _results;
230      _ref = this.children;
231      _results = [];
232      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
233        child = _ref[_i];
234        _results.push(child.show());
235      }
236      return _results;
237    };
238
239    return Node;
240  })();
241
242  Tree = (function() {
243    function Tree(table, settings) {
244      this.table = table;
245      this.settings = settings;
246      this.tree = {};
247
248      // Cache the nodes and roots in simple arrays for quick access/iteration
249      this.nodes = [];
250      this.roots = [];
251    }
252
253    Tree.prototype.collapseAll = function() {
254      var node, _i, _len, _ref, _results;
255      _ref = this.nodes;
256      _results = [];
257      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
258        node = _ref[_i];
259        _results.push(node.collapse());
260      }
261      return _results;
262    };
263
264    Tree.prototype.expandAll = function() {
265      var node, _i, _len, _ref, _results;
266      _ref = this.nodes;
267      _results = [];
268      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
269        node = _ref[_i];
270        _results.push(node.expand());
271      }
272      return _results;
273    };
274
275    Tree.prototype.findLastNode = function (node) {
276      if (node.children.length > 0) {
277        return this.findLastNode(node.children[node.children.length - 1]);
278      } else {
279        return node;
280      }
281    };
282
283    Tree.prototype.loadRows = function(rows) {
284      var node, row, i;
285
286      if (rows != null) {
287        for (i = 0; i < rows.length; i++) {
288          row = $(rows[i]);
289
290          if (row.data(this.settings.nodeIdAttr) != null) {
291            node = new Node(row, this.tree, this.settings);
292            this.nodes.push(node);
293            this.tree[node.id] = node;
294
295            if (node.parentId != null) {
296              this.tree[node.parentId].addChild(node);
297            } else {
298              this.roots.push(node);
299            }
300          }
301        }
302      }
303
304      for (i = 0; i < this.nodes.length; i++) {
305        node = this.nodes[i].updateBranchLeafClass();
306      }
307
308      return this;
309    };
310
311    Tree.prototype.move = function(node, destination) {
312      // Conditions:
313      // 1: +node+ should not be inserted as a child of +node+ itself.
314      // 2: +destination+ should not be the same as +node+'s current parent (this
315      //    prevents +node+ from being moved to the same location where it already
316      //    is).
317      // 3: +node+ should not be inserted in a location in a branch if this would
318      //    result in +node+ being an ancestor of itself.
319      var nodeParent = node.parentNode();
320      if (node !== destination && destination.id !== node.parentId && $.inArray(node, destination.ancestors()) === -1) {
321        node.setParent(destination);
322        this._moveRows(node, destination);
323
324        // Re-render parentNode if this is its first child node, and therefore
325        // doesn't have the expander yet.
326        if (node.parentNode().children.length === 1) {
327          node.parentNode().render();
328        }
329      }
330
331      if(nodeParent){
332        nodeParent.updateBranchLeafClass();
333      }
334      if(node.parentNode()){
335        node.parentNode().updateBranchLeafClass();
336      }
337      node.updateBranchLeafClass();
338      return this;
339    };
340
341    Tree.prototype.removeNode = function(node) {
342      // Recursively remove all descendants of +node+
343      this.unloadBranch(node);
344
345      // Remove node from DOM (<tr>)
346      node.row.remove();
347
348      // Clean up Tree object (so Node objects are GC-ed)
349      delete this.tree[node.id];
350      this.nodes.splice($.inArray(node, this.nodes), 1);
351    }
352
353    Tree.prototype.render = function() {
354      var root, _i, _len, _ref;
355      _ref = this.roots;
356      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
357        root = _ref[_i];
358
359        // Naming is confusing (show/render). I do not call render on node from
360        // here.
361        root.show();
362      }
363      return this;
364    };
365
366    Tree.prototype.sortBranch = function(node, sortFun) {
367      // First sort internal array of children
368      node.children.sort(sortFun);
369
370      // Next render rows in correct order on page
371      this._sortChildRows(node);
372
373      return this;
374    };
375
376    Tree.prototype.unloadBranch = function(node) {
377      var children, i;
378
379      for (i = 0; i < node.children.length; i++) {
380        this.removeNode(node.children[i]);
381      }
382
383      // Reset node's collection of children
384      node.children = [];
385
386      node.updateBranchLeafClass();
387
388      return this;
389    };
390
391    Tree.prototype._moveRows = function(node, destination) {
392      var children = node.children, i;
393
394      node.row.insertAfter(destination.row);
395      node.render();
396
397      // Loop backwards through children to have them end up on UI in correct
398      // order (see #112)
399      for (i = children.length - 1; i >= 0; i--) {
400        this._moveRows(children[i], node);
401      }
402    };
403
404    // Special _moveRows case, move children to itself to force sorting
405    Tree.prototype._sortChildRows = function(parentNode) {
406      return this._moveRows(parentNode, parentNode);
407    };
408
409    return Tree;
410  })();
411
412  // jQuery Plugin
413  methods = {
414    init: function(options, force) {
415      var settings;
416
417      settings = $.extend({
418        branchAttr: "ttBranch",
419        clickableNodeNames: false,
420        column: 0,
421        columnElType: "td", // i.e. 'td', 'th' or 'td,th'
422        expandable: false,
423        expanderTemplate: "<a href='#'>&nbsp;</a>",
424        indent: 10,
425        indenterTemplate: "<span class='indenter'></span>",
426        initialState: "collapsed",
427        nodeIdAttr: "ttId", // maps to data-tt-id
428        parentIdAttr: "ttParentId", // maps to data-tt-parent-id
429        stringExpand: "Expand",
430        stringCollapse: "Collapse",
431
432        // Events
433        onInitialized: null,
434        onNodeCollapse: null,
435        onNodeExpand: null,
436        onNodeInitialized: null
437      }, options);
438
439      return this.each(function() {
440        var el = $(this), tree;
441
442        if (force || el.data("treetable") === undefined) {
443          tree = new Tree(this, settings);
444          tree.loadRows(this.rows).render();
445
446          el.addClass("treetable").data("treetable", tree);
447
448          if (settings.onInitialized != null) {
449            settings.onInitialized.apply(tree);
450          }
451        }
452
453        return el;
454      });
455    },
456
457    destroy: function() {
458      return this.each(function() {
459        return $(this).removeData("treetable").removeClass("treetable");
460      });
461    },
462
463    collapseAll: function() {
464      this.data("treetable").collapseAll();
465      return this;
466    },
467
468    collapseNode: function(id) {
469      var node = this.data("treetable").tree[id];
470
471      if (node) {
472        node.collapse();
473      } else {
474        throw new Error("Unknown node '" + id + "'");
475      }
476
477      return this;
478    },
479
480    expandAll: function() {
481      this.data("treetable").expandAll();
482      return this;
483    },
484
485    expandNode: function(id) {
486      var node = this.data("treetable").tree[id];
487
488      if (node) {
489        if (!node.initialized) {
490          node._initialize();
491        }
492
493        node.expand();
494      } else {
495        throw new Error("Unknown node '" + id + "'");
496      }
497
498      return this;
499    },
500
501    loadBranch: function(node, rows) {
502      var settings = this.data("treetable").settings,
503          tree = this.data("treetable").tree;
504
505      // TODO Switch to $.parseHTML
506      rows = $(rows);
507
508      if (node == null) { // Inserting new root nodes
509        this.append(rows);
510      } else {
511        var lastNode = this.data("treetable").findLastNode(node);
512        rows.insertAfter(lastNode.row);
513      }
514
515      this.data("treetable").loadRows(rows);
516
517      // Make sure nodes are properly initialized
518      rows.filter("tr").each(function() {
519        tree[$(this).data(settings.nodeIdAttr)].show();
520      });
521
522      if (node != null) {
523        // Re-render parent to ensure expander icon is shown (#79)
524        node.render().expand();
525      }
526
527      return this;
528    },
529
530    move: function(nodeId, destinationId) {
531      var destination, node;
532
533      node = this.data("treetable").tree[nodeId];
534      destination = this.data("treetable").tree[destinationId];
535      this.data("treetable").move(node, destination);
536
537      return this;
538    },
539
540    node: function(id) {
541      return this.data("treetable").tree[id];
542    },
543
544    removeNode: function(id) {
545      var node = this.data("treetable").tree[id];
546
547      if (node) {
548        this.data("treetable").removeNode(node);
549      } else {
550        throw new Error("Unknown node '" + id + "'");
551      }
552
553      return this;
554    },
555
556    reveal: function(id) {
557      var node = this.data("treetable").tree[id];
558
559      if (node) {
560        node.reveal();
561      } else {
562        throw new Error("Unknown node '" + id + "'");
563      }
564
565      return this;
566    },
567
568    sortBranch: function(node, columnOrFunction) {
569      var settings = this.data("treetable").settings,
570          prepValue,
571          sortFun;
572
573      columnOrFunction = columnOrFunction || settings.column;
574      sortFun = columnOrFunction;
575
576      if ($.isNumeric(columnOrFunction)) {
577        sortFun = function(a, b) {
578          var extractValue, valA, valB;
579
580          extractValue = function(node) {
581            var val = node.row.find("td:eq(" + columnOrFunction + ")").text();
582            // Ignore trailing/leading whitespace and use uppercase values for
583            // case insensitive ordering
584            return $.trim(val).toUpperCase();
585          }
586
587          valA = extractValue(a);
588          valB = extractValue(b);
589
590          if (valA < valB) return -1;
591          if (valA > valB) return 1;
592          return 0;
593        };
594      }
595
596      this.data("treetable").sortBranch(node, sortFun);
597      return this;
598    },
599
600    unloadBranch: function(node) {
601      this.data("treetable").unloadBranch(node);
602      return this;
603    }
604  };
605
606  $.fn.treetable = function(method) {
607    if (methods[method]) {
608      return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
609    } else if (typeof method === 'object' || !method) {
610      return methods.init.apply(this, arguments);
611    } else {
612      return $.error("Method " + method + " does not exist on jQuery.treetable");
613    }
614  };
615
616  // Expose classes to world
617  this.TreeTable || (this.TreeTable = {});
618  this.TreeTable.Node = Node;
619  this.TreeTable.Tree = Tree;
620}).call(this);
621