jquery.treeTable-extended.js 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. /*!
  2. * Modified jQuery treeTable plugin
  3. * ================================
  4. * Based on jQuery treeTable Plugin 2.3.0
  5. *
  6. * http://ludo.cubicphuse.nl/jquery-plugins/treeTable/doc/
  7. *
  8. * Copyright 2011, Ludo van den Boom
  9. * Dual licensed under the MIT or GPL Version 2 licenses.
  10. *
  11. * Modifications:
  12. * - remove option clickableNodeNames
  13. * + add option clickableElement:
  14. * Specify element in row node on which expander event will be listened for.
  15. * (default) If not set event will be listened on the row itself.
  16. * + add option doubleclickMode:
  17. * If true, listen for double click event.
  18. * (default) If false, listen for click event.
  19. * + add option initialRootState:
  20. * Overwrites initialState setting for root nodes. Can be "expanded" or "collapsed".
  21. * (default) "collapsed"
  22. * + add option initialIndent:
  23. * Global indent modifier.
  24. * (default) 0
  25. * + add function $.fn.moveBranch:
  26. * Move +this+ node's entire branch before/after target element.
  27. * (arg) +where+ >> where to move the branch ("before" or "after")
  28. * (arg) +element+ >> target element
  29. * + add function $.fn.selectBranch:
  30. * Select +this+ node's entire branch.
  31. * + add function $.fn.branchLast:
  32. * Select +this+ node's entire branch last node.
  33. * + add function $.fn.nodeParent:
  34. * Return +this+ node's parent.
  35. * + add callback onNodeInit
  36. * (arg) +node+ >> initialized node
  37. * (arg) +expandable+ >> is node expandable?
  38. * (arg) +isRootNode+ >> is node a root node?
  39. * + add callback onNodeReinit
  40. * (arg) +node+ >> reinitialized node
  41. * (arg) +expandable+ >> is node expandable?
  42. * (arg) +isRootNode+ >> is node a root node?
  43. * Modified internal functions:
  44. * + added selectBranch function
  45. * + added branchLast function
  46. * - removed move function
  47. * + added insert function
  48. * + modified initialize function
  49. * + added reinitialize function
  50. * + modified indent function
  51. * + modified getPaddingLeft function
  52. *
  53. * Modified 2013, loostro:
  54. * Released under the MIT license.
  55. */
  56. (function($) {
  57. // Helps to make options available to all functions
  58. // TODO: This gives problems when there are both expandable and non-expandable
  59. // trees on a page. The options shouldn't be global to all these instances!
  60. var options;
  61. var defaultPaddingLeft;
  62. var persistStore;
  63. $.fn.treeTable = function(opts) {
  64. options = $.extend({}, $.fn.treeTable.defaults, opts);
  65. if(options.persist) {
  66. persistStore = new Persist.Store(options.persistStoreName);
  67. }
  68. return this.each(function() {
  69. $(this).addClass("treeTable").find("tbody tr").each(function() {
  70. // Skip initialized nodes.
  71. if (!$(this).hasClass('initialized')) {
  72. var isRootNode = ($(this)[0].className.search(options.childPrefix) == -1);
  73. // To optimize performance of indentation, I retrieve the padding-left
  74. // value of the first root node. This way I only have to call +css+
  75. // once.
  76. if (isRootNode && isNaN(defaultPaddingLeft)) {
  77. defaultPaddingLeft = options.initialIndent + parseInt($($(this).children("td")[options.treeColumn]).css('padding-left'), 10);
  78. }
  79. // Set child nodes to initial state if we're in expandable mode.
  80. if(!isRootNode && options.expandable && options.initialState == "collapsed") {
  81. $(this).addClass('ui-helper-hidden');
  82. }
  83. // If we're not in expandable mode, initialize all nodes.
  84. // If we're in expandable mode, only initialize root nodes.
  85. if(!options.expandable || isRootNode) {
  86. initialize($(this));
  87. }
  88. }
  89. });
  90. });
  91. };
  92. $.fn.treeTable.defaults = {
  93. childPrefix: "child-of-",
  94. clickableElement: false,
  95. doubleclickMode: false,
  96. expandable: true,
  97. indent: 19,
  98. initialIndent: 0,
  99. initialState: "collapsed",
  100. initialRootState: "collapsed",
  101. onNodeShow: null,
  102. onNodeHide: null,
  103. onExpandableInit: null,
  104. onNonExpandableInit: null,
  105. treeColumn: 0,
  106. persist: false,
  107. persistStoreName: 'treeTable',
  108. stringExpand: "Expand",
  109. stringCollapse: "Collapse"
  110. };
  111. //Expand all nodes
  112. $.fn.expandAll = function() {
  113. $(this).find("tr").each(function() {
  114. $(this).expand();
  115. });
  116. };
  117. //Collapse all nodes
  118. $.fn.collapseAll = function() {
  119. $(this).find("tr").each(function() {
  120. $(this).collapse();
  121. });
  122. };
  123. // Recursively hide all node's children in a tree
  124. $.fn.collapse = function() {
  125. return this.each(function() {
  126. $(this).removeClass("expanded").addClass("collapsed");
  127. if (options.persist) {
  128. persistNodeState($(this));
  129. }
  130. childrenOf($(this)).each(function() {
  131. if(!$(this).hasClass("collapsed")) {
  132. $(this).collapse();
  133. }
  134. $(this).addClass('ui-helper-hidden');
  135. if($.isFunction(options.onNodeHide)) {
  136. options.onNodeHide.call(this);
  137. }
  138. });
  139. });
  140. };
  141. // Recursively show all node's children in a tree
  142. $.fn.expand = function() {
  143. return this.each(function() {
  144. $(this).removeClass("collapsed").addClass("expanded");
  145. if (options.persist) {
  146. persistNodeState($(this));
  147. }
  148. childrenOf($(this)).each(function() {
  149. initialize($(this));
  150. if($(this).is(".expanded.parent")) {
  151. $(this).expand();
  152. }
  153. $(this).removeClass('ui-helper-hidden');
  154. if($.isFunction(options.onNodeShow)) {
  155. options.onNodeShow.call(this);
  156. }
  157. });
  158. });
  159. };
  160. // Reveal a node by expanding all ancestors
  161. $.fn.reveal = function() {
  162. $(ancestorsOf($(this)).reverse()).each(function() {
  163. initialize($(this));
  164. $(this).expand().show();
  165. });
  166. return this;
  167. };
  168. // Add an entire branch to +destination+
  169. $.fn.appendBranchTo = function(destination) {
  170. var node = $(this);
  171. var parent = parentOf(node);
  172. var target = $(destination);
  173. var ancestorNames = $.map(ancestorsOf(target), function(a) { return a.id; });
  174. // Conditions:
  175. // 1: +node+ should not be inserted in a location in a branch if this would
  176. // result in +node+ being an ancestor of itself.
  177. // 2: +node+ should not have a parent OR the destination should not be the
  178. // same as +node+'s current parent (this last condition prevents +node+
  179. // from being moved to the same location where it already is).
  180. // 3: +node+ should not be inserted as a child of +node+ itself.
  181. if($.inArray(node[0].id, ancestorNames) == -1 && (!parent || (target.id != parent[0].id)) && target.id != node[0].id) {
  182. insert(node, 'after', target); // Move nodes to new location
  183. if(parent) { node.removeClass(options.childPrefix + parent[0].id); } // Remove parent
  184. node.addClass(options.childPrefix + target[0].id); // Set new parent
  185. indent(node, ancestorsOf(node).length * options.indent); // Set new indentation
  186. }
  187. return this;
  188. };
  189. // Move +this+ node's entire branch before/after target +element+.
  190. $.fn.moveBranch = function(where, element) {
  191. // use appendBranchTo to handle 'in' action
  192. if(where == 'in') { $(this).appendBranchTo(element); return; }
  193. // sanity check
  194. if($.inArray(where, ['before','after']) == -1) { return; }
  195. var node = $(this);
  196. var parent = parentOf(node);
  197. var target = $(element);
  198. var targetParent = parentOf(target);
  199. var ancestorNames = $.map(ancestorsOf(target), function(a) { return a.id; });
  200. // Conditions:
  201. // 1: +node+ should not be inserted in a location in a branch if this would
  202. // result in +node+ being an ancestor of itself.
  203. // 2: +node+ should not be inserted before/after itself.
  204. if($.inArray(node[0].id, ancestorNames) == -1 && target[0].id != node[0].id) {
  205. insert(node, where, target); // Move nodes to new location
  206. if(parent) { node.removeClass(options.childPrefix + parent[0].id); } // Remove parent
  207. if(targetParent) { node.addClass(options.childPrefix + targetParent[0].id); } // Set new parent
  208. indent(node, ancestorsOf(node).length * options.indent); // Set new indentation
  209. }
  210. return this;
  211. };
  212. // Add reverse() function from JS Arrays
  213. $.fn.reverse = function() {
  214. return this.pushStack(this.get().reverse(), arguments);
  215. };
  216. // Toggle an entire branch
  217. $.fn.toggleBranch = function() {
  218. if($(this).hasClass("collapsed")) {
  219. $(this).expand();
  220. } else {
  221. $(this).collapse();
  222. }
  223. return this;
  224. };
  225. // Get node's parent
  226. $.fn.nodeParent = function () {
  227. var $node = $(this);
  228. var match = $node[0].className.match(new RegExp(options.childPrefix+'[^\\s]+', 'i'));
  229. return (match) ? $('#node-'+match[0].substring(14)) : null;
  230. }
  231. // Reinitialize node
  232. $.fn.nodeReinitialize = function() {
  233. reinitialize($(this));
  234. return this;
  235. };
  236. // Select an entire branch
  237. $.fn.selectBranch = function() {
  238. return selectBranch(this);
  239. };
  240. // Select branch last node
  241. $.fn.branchLast = function() {
  242. return branchLast(this);
  243. };
  244. // === Private functions
  245. function ancestorsOf(node) {
  246. var ancestors = [];
  247. while(node = parentOf(node)) {
  248. ancestors[ancestors.length] = node[0];
  249. }
  250. return ancestors;
  251. };
  252. function childrenOf(node) {
  253. return $(node).siblings("tr." + options.childPrefix + node[0].id);
  254. };
  255. // note: this function assumes that
  256. // last node of branch is the one with the highest index
  257. function branchLast(node) {
  258. return (childrenOf(node).length) ? branchLast(childrenOf(node).last()) : $(node);
  259. };
  260. // note: this function assumes that
  261. // all nodes between node and branchLast(node) belong to this branch
  262. // that is true after initializing treeTable and as long as you use only
  263. // provided API to move treeTable rows
  264. //
  265. // If node has no children, return that node
  266. function selectBranch(node) {
  267. var nodelast = branchLast(node);
  268. return (node[0].id === nodelast[0].id) ? node : node.nextUntil(nodelast).addBack().add(nodelast);
  269. };
  270. function getPaddingLeft(node) {
  271. return ancestorsOf(node).length * options.indent;
  272. }
  273. function indent(node, value) {
  274. var cell = $(node.children("td")[options.treeColumn]);
  275. cell[0].style.paddingLeft = options.initialIndent + value + 'px';
  276. childrenOf(node).each(function() {
  277. indent($(this), value + options.indent);
  278. });
  279. };
  280. function initialize(node) {
  281. if(!node.hasClass("initialized")) {
  282. node.addClass("initialized");
  283. var isRootNode = (node[0].className.search(options.childPrefix) == -1);
  284. var childNodes = childrenOf(node);
  285. var expandable = childNodes.length > 0;
  286. if(!node.hasClass("parent") && expandable) {
  287. node.addClass("parent");
  288. }
  289. if($.isFunction(options.onNodeInit)) {
  290. options.onNodeInit.call(this, node, expandable, isRootNode);
  291. }
  292. if(expandable) {
  293. indent(node, getPaddingLeft(node));
  294. if(options.expandable) {
  295. var handle = (options.clickableElement) ? node.find(options.clickableElement) : node;
  296. handle.attr('title', options.stringExpand).addClass('expander');
  297. handle.on((options.doubleclickMode) ? 'dblclick' : 'click', function(e){
  298. e.preventDefault;
  299. node.toggleBranch();
  300. });
  301. if (options.persist && getPersistedNodeState(node)) {
  302. node.addClass('expanded');
  303. }
  304. // Check for a class set explicitly by the user, otherwise set the default class
  305. if(!(node.hasClass("expanded") || node.hasClass("collapsed"))) {
  306. (isRootNode) ? node.addClass(options.initialRootState) : node.addClass(options.initialState);
  307. }
  308. if(node.hasClass("expanded")) {
  309. node.expand();
  310. }
  311. }
  312. }
  313. }
  314. };
  315. function reinitialize(node) {
  316. if(node.hasClass("initialized")) {
  317. node.removeClass('initialized').removeClass('parent')
  318. .removeClass('expanded').removeClass('collapsed');
  319. var isRootNode = (node[0].className.search(options.childPrefix) == -1);
  320. var childNodes = childrenOf(node);
  321. var expandable = childNodes.length > 0;
  322. if(options.expandable) {
  323. var handle = (options.clickableElement) ? node.find(options.clickableElement) : node;
  324. handle.removeAttr('title').removeClass('expander');
  325. handle.off((options.doubleclickMode) ? 'dblclick' : 'click');
  326. }
  327. if($.isFunction(options.onNodeReinit)) {
  328. options.onNodeReinit.call(this, node, expandable, isRootNode);
  329. }
  330. if(expandable) { node.addClass('expanded'); }
  331. initialize(node);
  332. }
  333. };
  334. // note: this function assumes that
  335. // all nodes between node and branchLast(node) belong to this branch
  336. // that is true after initializing treeTable and as long as you use only
  337. // provided API to move treeTable rows
  338. function insert(node, where, target) {
  339. if(where == 'before') {
  340. // no problems here
  341. // simply insert before +target+
  342. selectBranch(node).insertBefore(target);
  343. }
  344. if(where == 'after') {
  345. // target may have children
  346. // insert after +target+ last child
  347. var targetlast = branchLast(target);
  348. selectBranch(node).insertAfter(targetlast);
  349. }
  350. };
  351. function parentOf(node) {
  352. var classNames = node[0].className.split(' ');
  353. for(var key=0; key<classNames.length; key++) {
  354. if(classNames[key].match(options.childPrefix)) {
  355. return $(node).siblings("#" + classNames[key].substring(options.childPrefix.length));
  356. }
  357. }
  358. return null;
  359. };
  360. //saving state functions, not critical, so will not generate alerts on error
  361. function persistNodeState(node) {
  362. if(node.hasClass('expanded')) {
  363. try {
  364. persistStore.set(node.attr('id'), '1');
  365. } catch (err) {
  366. }
  367. } else {
  368. try {
  369. persistStore.remove(node.attr('id'));
  370. } catch (err) {
  371. }
  372. }
  373. }
  374. function getPersistedNodeState(node) {
  375. try {
  376. return persistStore.get(node.attr('id')) == '1';
  377. } catch (err) {
  378. return false;
  379. }
  380. }
  381. })(jQuery);