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='#'> </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