You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

629 lines
16 KiB

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