bootstrap-treeview.js 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249
  1. /* =========================================================
  2. * bootstrap-treeview.js v1.2.0
  3. * =========================================================
  4. * Copyright 2013 Jonathan Miles
  5. * Project URL : http://www.jondmiles.com/bootstrap-treeview
  6. *
  7. * Licensed under the Apache License, Version 2.0 (the "License");
  8. * you may not use this file except in compliance with the License.
  9. * You may obtain a copy of the License at
  10. *
  11. * http://www.apache.org/licenses/LICENSE-2.0
  12. *
  13. * Unless required by applicable law or agreed to in writing, software
  14. * distributed under the License is distributed on an "AS IS" BASIS,
  15. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  16. * See the License for the specific language governing permissions and
  17. * limitations under the License.
  18. * ========================================================= */
  19. ;(function ($, window, document, undefined) {
  20. /*global jQuery, console*/
  21. 'use strict';
  22. var pluginName = 'treeview';
  23. var _default = {};
  24. _default.settings = {
  25. injectStyle: true,
  26. levels: 2,
  27. expandIcon: 'glyphicon glyphicon-plus',
  28. collapseIcon: 'glyphicon glyphicon-minus',
  29. emptyIcon: 'glyphicon',
  30. nodeIcon: '',
  31. selectedIcon: '',
  32. checkedIcon: 'glyphicon glyphicon-check',
  33. uncheckedIcon: 'glyphicon glyphicon-unchecked',
  34. color: undefined, // '#000000',
  35. backColor: undefined, // '#FFFFFF',
  36. borderColor: undefined, // '#dddddd',
  37. onhoverColor: '#F5F5F5',
  38. selectedColor: '#FFFFFF',
  39. selectedBackColor: '#428bca',
  40. searchResultColor: '#D9534F',
  41. searchResultBackColor: undefined, //'#FFFFFF',
  42. enableLinks: false,
  43. highlightSelected: true,
  44. highlightSearchResults: true,
  45. showBorder: true,
  46. showIcon: true,
  47. showCheckbox: false,
  48. showTags: false,
  49. multiSelect: false,
  50. // Event handlers
  51. onNodeChecked: undefined,
  52. onNodeCollapsed: undefined,
  53. onNodeDisabled: undefined,
  54. onNodeEnabled: undefined,
  55. onNodeExpanded: undefined,
  56. onNodeSelected: undefined,
  57. onNodeUnchecked: undefined,
  58. onNodeUnselected: undefined,
  59. onSearchComplete: undefined,
  60. onSearchCleared: undefined
  61. };
  62. _default.options = {
  63. silent: false,
  64. ignoreChildren: false
  65. };
  66. _default.searchOptions = {
  67. ignoreCase: true,
  68. exactMatch: false,
  69. revealResults: true
  70. };
  71. var Tree = function (element, options) {
  72. this.$element = $(element);
  73. this.elementId = element.id;
  74. this.styleId = this.elementId + '-style';
  75. this.init(options);
  76. return {
  77. // Options (public access)
  78. options: this.options,
  79. // Initialize / destroy methods
  80. init: $.proxy(this.init, this),
  81. remove: $.proxy(this.remove, this),
  82. // Get methods
  83. getNode: $.proxy(this.getNode, this),
  84. getParent: $.proxy(this.getParent, this),
  85. getSiblings: $.proxy(this.getSiblings, this),
  86. getSelected: $.proxy(this.getSelected, this),
  87. getUnselected: $.proxy(this.getUnselected, this),
  88. getExpanded: $.proxy(this.getExpanded, this),
  89. getCollapsed: $.proxy(this.getCollapsed, this),
  90. getChecked: $.proxy(this.getChecked, this),
  91. getUnchecked: $.proxy(this.getUnchecked, this),
  92. getDisabled: $.proxy(this.getDisabled, this),
  93. getEnabled: $.proxy(this.getEnabled, this),
  94. // Select methods
  95. selectNode: $.proxy(this.selectNode, this),
  96. unselectNode: $.proxy(this.unselectNode, this),
  97. toggleNodeSelected: $.proxy(this.toggleNodeSelected, this),
  98. // Expand / collapse methods
  99. collapseAll: $.proxy(this.collapseAll, this),
  100. collapseNode: $.proxy(this.collapseNode, this),
  101. expandAll: $.proxy(this.expandAll, this),
  102. expandNode: $.proxy(this.expandNode, this),
  103. toggleNodeExpanded: $.proxy(this.toggleNodeExpanded, this),
  104. revealNode: $.proxy(this.revealNode, this),
  105. // Expand / collapse methods
  106. checkAll: $.proxy(this.checkAll, this),
  107. checkNode: $.proxy(this.checkNode, this),
  108. uncheckAll: $.proxy(this.uncheckAll, this),
  109. uncheckNode: $.proxy(this.uncheckNode, this),
  110. toggleNodeChecked: $.proxy(this.toggleNodeChecked, this),
  111. // Disable / enable methods
  112. disableAll: $.proxy(this.disableAll, this),
  113. disableNode: $.proxy(this.disableNode, this),
  114. enableAll: $.proxy(this.enableAll, this),
  115. enableNode: $.proxy(this.enableNode, this),
  116. toggleNodeDisabled: $.proxy(this.toggleNodeDisabled, this),
  117. // Search methods
  118. search: $.proxy(this.search, this),
  119. clearSearch: $.proxy(this.clearSearch, this)
  120. };
  121. };
  122. Tree.prototype.init = function (options) {
  123. this.tree = [];
  124. this.nodes = [];
  125. if (options.data) {
  126. if (typeof options.data === 'string') {
  127. options.data = $.parseJSON(options.data);
  128. }
  129. this.tree = $.extend(true, [], options.data);
  130. delete options.data;
  131. }
  132. this.options = $.extend({}, _default.settings, options);
  133. this.destroy();
  134. this.subscribeEvents();
  135. this.setInitialStates({ nodes: this.tree }, 0);
  136. this.render();
  137. };
  138. Tree.prototype.remove = function () {
  139. this.destroy();
  140. $.removeData(this, pluginName);
  141. $('#' + this.styleId).remove();
  142. };
  143. Tree.prototype.destroy = function () {
  144. if (!this.initialized) return;
  145. this.$wrapper.remove();
  146. this.$wrapper = null;
  147. // Switch off events
  148. this.unsubscribeEvents();
  149. // Reset this.initialized flag
  150. this.initialized = false;
  151. };
  152. Tree.prototype.unsubscribeEvents = function () {
  153. this.$element.off('click');
  154. this.$element.off('nodeChecked');
  155. this.$element.off('nodeCollapsed');
  156. this.$element.off('nodeDisabled');
  157. this.$element.off('nodeEnabled');
  158. this.$element.off('nodeExpanded');
  159. this.$element.off('nodeSelected');
  160. this.$element.off('nodeUnchecked');
  161. this.$element.off('nodeUnselected');
  162. this.$element.off('searchComplete');
  163. this.$element.off('searchCleared');
  164. };
  165. Tree.prototype.subscribeEvents = function () {
  166. this.unsubscribeEvents();
  167. this.$element.on('click', $.proxy(this.clickHandler, this));
  168. if (typeof (this.options.onNodeChecked) === 'function') {
  169. this.$element.on('nodeChecked', this.options.onNodeChecked);
  170. }
  171. if (typeof (this.options.onNodeCollapsed) === 'function') {
  172. this.$element.on('nodeCollapsed', this.options.onNodeCollapsed);
  173. }
  174. if (typeof (this.options.onNodeDisabled) === 'function') {
  175. this.$element.on('nodeDisabled', this.options.onNodeDisabled);
  176. }
  177. if (typeof (this.options.onNodeEnabled) === 'function') {
  178. this.$element.on('nodeEnabled', this.options.onNodeEnabled);
  179. }
  180. if (typeof (this.options.onNodeExpanded) === 'function') {
  181. this.$element.on('nodeExpanded', this.options.onNodeExpanded);
  182. }
  183. if (typeof (this.options.onNodeSelected) === 'function') {
  184. this.$element.on('nodeSelected', this.options.onNodeSelected);
  185. }
  186. if (typeof (this.options.onNodeUnchecked) === 'function') {
  187. this.$element.on('nodeUnchecked', this.options.onNodeUnchecked);
  188. }
  189. if (typeof (this.options.onNodeUnselected) === 'function') {
  190. this.$element.on('nodeUnselected', this.options.onNodeUnselected);
  191. }
  192. if (typeof (this.options.onSearchComplete) === 'function') {
  193. this.$element.on('searchComplete', this.options.onSearchComplete);
  194. }
  195. if (typeof (this.options.onSearchCleared) === 'function') {
  196. this.$element.on('searchCleared', this.options.onSearchCleared);
  197. }
  198. };
  199. /*
  200. Recurse the tree structure and ensure all nodes have
  201. valid initial states. User defined states will be preserved.
  202. For performance we also take this opportunity to
  203. index nodes in a flattened structure
  204. */
  205. Tree.prototype.setInitialStates = function (node, level) {
  206. if (!node.nodes) return;
  207. level += 1;
  208. var parent = node;
  209. var _this = this;
  210. $.each(node.nodes, function checkStates(index, node) {
  211. // nodeId : unique, incremental identifier
  212. node.nodeId = _this.nodes.length;
  213. // parentId : transversing up the tree
  214. node.parentId = parent.nodeId;
  215. // if not provided set selectable default value
  216. if (!node.hasOwnProperty('selectable')) {
  217. node.selectable = true;
  218. }
  219. // where provided we should preserve states
  220. node.state = node.state || {};
  221. // set checked state; unless set always false
  222. if (!node.state.hasOwnProperty('checked')) {
  223. node.state.checked = false;
  224. }
  225. // set enabled state; unless set always false
  226. if (!node.state.hasOwnProperty('disabled')) {
  227. node.state.disabled = false;
  228. }
  229. // set expanded state; if not provided based on levels
  230. if (!node.state.hasOwnProperty('expanded')) {
  231. if (!node.state.disabled &&
  232. (level < _this.options.levels) &&
  233. (node.nodes && node.nodes.length > 0)) {
  234. node.state.expanded = true;
  235. }
  236. else {
  237. node.state.expanded = false;
  238. }
  239. }
  240. // set selected state; unless set always false
  241. if (!node.state.hasOwnProperty('selected')) {
  242. node.state.selected = false;
  243. }
  244. // index nodes in a flattened structure for use later
  245. _this.nodes.push(node);
  246. // recurse child nodes and transverse the tree
  247. if (node.nodes) {
  248. _this.setInitialStates(node, level);
  249. }
  250. });
  251. };
  252. Tree.prototype.clickHandler = function (event) {
  253. if (!this.options.enableLinks) event.preventDefault();
  254. var target = $(event.target);
  255. var node = this.findNode(target);
  256. if (!node || node.state.disabled) return;
  257. var classList = target.attr('class') ? target.attr('class').split(' ') : [];
  258. if ((classList.indexOf('expand-icon') !== -1)) {
  259. this.toggleExpandedState(node, _default.options);
  260. this.render();
  261. }
  262. else if ((classList.indexOf('check-icon') !== -1)) {
  263. this.toggleCheckedState(node, _default.options);
  264. this.render();
  265. }
  266. else {
  267. if (node.selectable) {
  268. this.toggleSelectedState(node, _default.options);
  269. } else {
  270. this.toggleExpandedState(node, _default.options);
  271. }
  272. this.render();
  273. }
  274. };
  275. // Looks up the DOM for the closest parent list item to retrieve the
  276. // data attribute nodeid, which is used to lookup the node in the flattened structure.
  277. Tree.prototype.findNode = function (target) {
  278. var nodeId = target.closest('li.list-group-item').attr('data-nodeid');
  279. var node = this.nodes[nodeId];
  280. if (!node) {
  281. console.log('Error: node does not exist');
  282. }
  283. return node;
  284. };
  285. Tree.prototype.toggleExpandedState = function (node, options) {
  286. if (!node) return;
  287. this.setExpandedState(node, !node.state.expanded, options);
  288. };
  289. Tree.prototype.setExpandedState = function (node, state, options) {
  290. if (state === node.state.expanded) return;
  291. if (state && node.nodes) {
  292. // Expand a node
  293. node.state.expanded = true;
  294. if (!options.silent) {
  295. this.$element.trigger('nodeExpanded', $.extend(true, {}, node));
  296. }
  297. }
  298. else if (!state) {
  299. // Collapse a node
  300. node.state.expanded = false;
  301. if (!options.silent) {
  302. this.$element.trigger('nodeCollapsed', $.extend(true, {}, node));
  303. }
  304. // Collapse child nodes
  305. if (node.nodes && !options.ignoreChildren) {
  306. $.each(node.nodes, $.proxy(function (index, node) {
  307. this.setExpandedState(node, false, options);
  308. }, this));
  309. }
  310. }
  311. };
  312. Tree.prototype.toggleSelectedState = function (node, options) {
  313. if (!node) return;
  314. this.setSelectedState(node, !node.state.selected, options);
  315. };
  316. Tree.prototype.setSelectedState = function (node, state, options) {
  317. if (state === node.state.selected) return;
  318. if (state) {
  319. // If multiSelect false, unselect previously selected
  320. if (!this.options.multiSelect) {
  321. $.each(this.findNodes('true', 'g', 'state.selected'), $.proxy(function (index, node) {
  322. this.setSelectedState(node, false, options);
  323. }, this));
  324. }
  325. // Continue selecting node
  326. node.state.selected = true;
  327. if (!options.silent) {
  328. this.$element.trigger('nodeSelected', $.extend(true, {}, node));
  329. }
  330. }
  331. else {
  332. // Unselect node
  333. node.state.selected = false;
  334. if (!options.silent) {
  335. this.$element.trigger('nodeUnselected', $.extend(true, {}, node));
  336. }
  337. }
  338. };
  339. Tree.prototype.toggleCheckedState = function (node, options) {
  340. if (!node) return;
  341. this.setCheckedState(node, !node.state.checked, options);
  342. };
  343. Tree.prototype.setCheckedState = function (node, state, options) {
  344. if (state === node.state.checked) return;
  345. if (state) {
  346. // Check node
  347. node.state.checked = true;
  348. if (!options.silent) {
  349. this.$element.trigger('nodeChecked', $.extend(true, {}, node));
  350. }
  351. }
  352. else {
  353. // Uncheck node
  354. node.state.checked = false;
  355. if (!options.silent) {
  356. this.$element.trigger('nodeUnchecked', $.extend(true, {}, node));
  357. }
  358. }
  359. };
  360. Tree.prototype.setDisabledState = function (node, state, options) {
  361. if (state === node.state.disabled) return;
  362. if (state) {
  363. // Disable node
  364. node.state.disabled = true;
  365. // Disable all other states
  366. this.setExpandedState(node, false, options);
  367. this.setSelectedState(node, false, options);
  368. this.setCheckedState(node, false, options);
  369. if (!options.silent) {
  370. this.$element.trigger('nodeDisabled', $.extend(true, {}, node));
  371. }
  372. }
  373. else {
  374. // Enabled node
  375. node.state.disabled = false;
  376. if (!options.silent) {
  377. this.$element.trigger('nodeEnabled', $.extend(true, {}, node));
  378. }
  379. }
  380. };
  381. Tree.prototype.render = function () {
  382. if (!this.initialized) {
  383. // Setup first time only components
  384. this.$element.addClass(pluginName);
  385. this.$wrapper = $(this.template.list);
  386. this.injectStyle();
  387. this.initialized = true;
  388. }
  389. this.$element.empty().append(this.$wrapper.empty());
  390. // Build tree
  391. this.buildTree(this.tree, 0);
  392. };
  393. // Starting from the root node, and recursing down the
  394. // structure we build the tree one node at a time
  395. Tree.prototype.buildTree = function (nodes, level) {
  396. if (!nodes) return;
  397. level += 1;
  398. var _this = this;
  399. $.each(nodes, function addNodes(id, node) {
  400. var treeItem = $(_this.template.item)
  401. .addClass('node-' + _this.elementId)
  402. .addClass(node.state.checked ? 'node-checked' : '')
  403. .addClass(node.state.disabled ? 'node-disabled': '')
  404. .addClass(node.state.selected ? 'node-selected' : '')
  405. .addClass(node.searchResult ? 'search-result' : '')
  406. .attr('data-nodeid', node.nodeId)
  407. .attr('style', _this.buildStyleOverride(node));
  408. // Add indent/spacer to mimic tree structure
  409. for (var i = 0; i < (level - 1); i++) {
  410. treeItem.append(_this.template.indent);
  411. }
  412. // Add expand, collapse or empty spacer icons
  413. var classList = [];
  414. if (node.nodes) {
  415. classList.push('expand-icon');
  416. if (node.state.expanded) {
  417. classList.push(_this.options.collapseIcon);
  418. }
  419. else {
  420. classList.push(_this.options.expandIcon);
  421. }
  422. }
  423. else {
  424. classList.push(_this.options.emptyIcon);
  425. }
  426. treeItem
  427. .append($(_this.template.icon)
  428. .addClass(classList.join(' '))
  429. );
  430. // Add node icon
  431. if (_this.options.showIcon) {
  432. var classList = ['node-icon'];
  433. classList.push(node.icon || _this.options.nodeIcon);
  434. if (node.state.selected) {
  435. classList.pop();
  436. classList.push(node.selectedIcon || _this.options.selectedIcon ||
  437. node.icon || _this.options.nodeIcon);
  438. }
  439. treeItem
  440. .append($(_this.template.icon)
  441. .addClass(classList.join(' '))
  442. );
  443. }
  444. // Add check / unchecked icon
  445. if (_this.options.showCheckbox) {
  446. var classList = ['check-icon'];
  447. if (node.state.checked) {
  448. classList.push(_this.options.checkedIcon);
  449. }
  450. else {
  451. classList.push(_this.options.uncheckedIcon);
  452. }
  453. treeItem
  454. .append($(_this.template.icon)
  455. .addClass(classList.join(' '))
  456. );
  457. }
  458. // Add text
  459. if (_this.options.enableLinks) {
  460. // Add hyperlink
  461. treeItem
  462. .append($(_this.template.link)
  463. .attr('href', node.href)
  464. .append(node.text)
  465. );
  466. }
  467. else {
  468. // otherwise just text
  469. treeItem
  470. .append(node.text);
  471. }
  472. // Add tags as badges
  473. if (_this.options.showTags && node.tags) {
  474. $.each(node.tags, function addTag(id, tag) {
  475. treeItem
  476. .append($(_this.template.badge)
  477. .append(tag)
  478. );
  479. });
  480. }
  481. // Add item to the tree
  482. _this.$wrapper.append(treeItem);
  483. // Recursively add child ndoes
  484. if (node.nodes && node.state.expanded && !node.state.disabled) {
  485. return _this.buildTree(node.nodes, level);
  486. }
  487. });
  488. };
  489. // Define any node level style override for
  490. // 1. selectedNode
  491. // 2. node|data assigned color overrides
  492. Tree.prototype.buildStyleOverride = function (node) {
  493. if (node.state.disabled) return '';
  494. var color = node.color;
  495. var backColor = node.backColor;
  496. if (this.options.highlightSelected && node.state.selected) {
  497. if (this.options.selectedColor) {
  498. color = this.options.selectedColor;
  499. }
  500. if (this.options.selectedBackColor) {
  501. backColor = this.options.selectedBackColor;
  502. }
  503. }
  504. if (this.options.highlightSearchResults && node.searchResult && !node.state.disabled) {
  505. if (this.options.searchResultColor) {
  506. color = this.options.searchResultColor;
  507. }
  508. if (this.options.searchResultBackColor) {
  509. backColor = this.options.searchResultBackColor;
  510. }
  511. }
  512. return 'color:' + color +
  513. ';background-color:' + backColor + ';';
  514. };
  515. // Add inline style into head
  516. Tree.prototype.injectStyle = function () {
  517. if (this.options.injectStyle && !document.getElementById(this.styleId)) {
  518. $('<style type="text/css" id="' + this.styleId + '"> ' + this.buildStyle() + ' </style>').appendTo('head');
  519. }
  520. };
  521. // Construct trees style based on user options
  522. Tree.prototype.buildStyle = function () {
  523. var style = '.node-' + this.elementId + '{';
  524. if (this.options.color) {
  525. style += 'color:' + this.options.color + ';';
  526. }
  527. if (this.options.backColor) {
  528. style += 'background-color:' + this.options.backColor + ';';
  529. }
  530. if (!this.options.showBorder) {
  531. style += 'border:none;';
  532. }
  533. else if (this.options.borderColor) {
  534. style += 'border:1px solid ' + this.options.borderColor + ';';
  535. }
  536. style += '}';
  537. if (this.options.onhoverColor) {
  538. style += '.node-' + this.elementId + ':not(.node-disabled):hover{' +
  539. 'background-color:' + this.options.onhoverColor + ';' +
  540. '}';
  541. }
  542. return this.css + style;
  543. };
  544. Tree.prototype.template = {
  545. list: '<ul class="list-group"></ul>',
  546. item: '<li class="list-group-item"></li>',
  547. indent: '<span class="indent"></span>',
  548. icon: '<span class="icon"></span>',
  549. link: '<a href="#" style="color:inherit;"></a>',
  550. badge: '<span class="badge"></span>'
  551. };
  552. Tree.prototype.css = '.treeview .list-group-item{cursor:pointer}.treeview span.indent{margin-left:10px;margin-right:10px}.treeview span.icon{width:12px;margin-right:5px}.treeview .node-disabled{color:silver;cursor:not-allowed}'
  553. /**
  554. Returns a single node object that matches the given node id.
  555. @param {Number} nodeId - A node's unique identifier
  556. @return {Object} node - Matching node
  557. */
  558. Tree.prototype.getNode = function (nodeId) {
  559. return this.nodes[nodeId];
  560. };
  561. /**
  562. Returns the parent node of a given node, if valid otherwise returns undefined.
  563. @param {Object|Number} identifier - A valid node or node id
  564. @returns {Object} node - The parent node
  565. */
  566. Tree.prototype.getParent = function (identifier) {
  567. var node = this.identifyNode(identifier);
  568. return this.nodes[node.parentId];
  569. };
  570. /**
  571. Returns an array of sibling nodes for a given node, if valid otherwise returns undefined.
  572. @param {Object|Number} identifier - A valid node or node id
  573. @returns {Array} nodes - Sibling nodes
  574. */
  575. Tree.prototype.getSiblings = function (identifier) {
  576. var node = this.identifyNode(identifier);
  577. var parent = this.getParent(node);
  578. var nodes = parent ? parent.nodes : this.tree;
  579. return nodes.filter(function (obj) {
  580. return obj.nodeId !== node.nodeId;
  581. });
  582. };
  583. /**
  584. Returns an array of selected nodes.
  585. @returns {Array} nodes - Selected nodes
  586. */
  587. Tree.prototype.getSelected = function () {
  588. return this.findNodes('true', 'g', 'state.selected');
  589. };
  590. /**
  591. Returns an array of unselected nodes.
  592. @returns {Array} nodes - Unselected nodes
  593. */
  594. Tree.prototype.getUnselected = function () {
  595. return this.findNodes('false', 'g', 'state.selected');
  596. };
  597. /**
  598. Returns an array of expanded nodes.
  599. @returns {Array} nodes - Expanded nodes
  600. */
  601. Tree.prototype.getExpanded = function () {
  602. return this.findNodes('true', 'g', 'state.expanded');
  603. };
  604. /**
  605. Returns an array of collapsed nodes.
  606. @returns {Array} nodes - Collapsed nodes
  607. */
  608. Tree.prototype.getCollapsed = function () {
  609. return this.findNodes('false', 'g', 'state.expanded');
  610. };
  611. /**
  612. Returns an array of checked nodes.
  613. @returns {Array} nodes - Checked nodes
  614. */
  615. Tree.prototype.getChecked = function () {
  616. return this.findNodes('true', 'g', 'state.checked');
  617. };
  618. /**
  619. Returns an array of unchecked nodes.
  620. @returns {Array} nodes - Unchecked nodes
  621. */
  622. Tree.prototype.getUnchecked = function () {
  623. return this.findNodes('false', 'g', 'state.checked');
  624. };
  625. /**
  626. Returns an array of disabled nodes.
  627. @returns {Array} nodes - Disabled nodes
  628. */
  629. Tree.prototype.getDisabled = function () {
  630. return this.findNodes('true', 'g', 'state.disabled');
  631. };
  632. /**
  633. Returns an array of enabled nodes.
  634. @returns {Array} nodes - Enabled nodes
  635. */
  636. Tree.prototype.getEnabled = function () {
  637. return this.findNodes('false', 'g', 'state.disabled');
  638. };
  639. /**
  640. Set a node state to selected
  641. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  642. @param {optional Object} options
  643. */
  644. Tree.prototype.selectNode = function (identifiers, options) {
  645. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  646. this.setSelectedState(node, true, options);
  647. }, this));
  648. this.render();
  649. };
  650. /**
  651. Set a node state to unselected
  652. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  653. @param {optional Object} options
  654. */
  655. Tree.prototype.unselectNode = function (identifiers, options) {
  656. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  657. this.setSelectedState(node, false, options);
  658. }, this));
  659. this.render();
  660. };
  661. /**
  662. Toggles a node selected state; selecting if unselected, unselecting if selected.
  663. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  664. @param {optional Object} options
  665. */
  666. Tree.prototype.toggleNodeSelected = function (identifiers, options) {
  667. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  668. this.toggleSelectedState(node, options);
  669. }, this));
  670. this.render();
  671. };
  672. /**
  673. Collapse all tree nodes
  674. @param {optional Object} options
  675. */
  676. Tree.prototype.collapseAll = function (options) {
  677. var identifiers = this.findNodes('true', 'g', 'state.expanded');
  678. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  679. this.setExpandedState(node, false, options);
  680. }, this));
  681. this.render();
  682. };
  683. /**
  684. Collapse a given tree node
  685. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  686. @param {optional Object} options
  687. */
  688. Tree.prototype.collapseNode = function (identifiers, options) {
  689. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  690. this.setExpandedState(node, false, options);
  691. }, this));
  692. this.render();
  693. };
  694. /**
  695. Expand all tree nodes
  696. @param {optional Object} options
  697. */
  698. Tree.prototype.expandAll = function (options) {
  699. options = $.extend({}, _default.options, options);
  700. if (options && options.levels) {
  701. this.expandLevels(this.tree, options.levels, options);
  702. }
  703. else {
  704. var identifiers = this.findNodes('false', 'g', 'state.expanded');
  705. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  706. this.setExpandedState(node, true, options);
  707. }, this));
  708. }
  709. this.render();
  710. };
  711. /**
  712. Expand a given tree node
  713. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  714. @param {optional Object} options
  715. */
  716. Tree.prototype.expandNode = function (identifiers, options) {
  717. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  718. this.setExpandedState(node, true, options);
  719. if (node.nodes && (options && options.levels)) {
  720. this.expandLevels(node.nodes, options.levels-1, options);
  721. }
  722. }, this));
  723. this.render();
  724. };
  725. Tree.prototype.expandLevels = function (nodes, level, options) {
  726. options = $.extend({}, _default.options, options);
  727. $.each(nodes, $.proxy(function (index, node) {
  728. this.setExpandedState(node, (level > 0) ? true : false, options);
  729. if (node.nodes) {
  730. this.expandLevels(node.nodes, level-1, options);
  731. }
  732. }, this));
  733. };
  734. /**
  735. Reveals a given tree node, expanding the tree from node to root.
  736. @param {Object|Number|Array} identifiers - A valid node, node id or array of node identifiers
  737. @param {optional Object} options
  738. */
  739. Tree.prototype.revealNode = function (identifiers, options) {
  740. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  741. var parentNode = this.getParent(node);
  742. while (parentNode) {
  743. this.setExpandedState(parentNode, true, options);
  744. parentNode = this.getParent(parentNode);
  745. };
  746. }, this));
  747. this.render();
  748. };
  749. /**
  750. Toggles a nodes expanded state; collapsing if expanded, expanding if collapsed.
  751. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  752. @param {optional Object} options
  753. */
  754. Tree.prototype.toggleNodeExpanded = function (identifiers, options) {
  755. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  756. this.toggleExpandedState(node, options);
  757. }, this));
  758. this.render();
  759. };
  760. /**
  761. Check all tree nodes
  762. @param {optional Object} options
  763. */
  764. Tree.prototype.checkAll = function (options) {
  765. var identifiers = this.findNodes('false', 'g', 'state.checked');
  766. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  767. this.setCheckedState(node, true, options);
  768. }, this));
  769. this.render();
  770. };
  771. /**
  772. Check a given tree node
  773. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  774. @param {optional Object} options
  775. */
  776. Tree.prototype.checkNode = function (identifiers, options) {
  777. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  778. this.setCheckedState(node, true, options);
  779. }, this));
  780. this.render();
  781. };
  782. /**
  783. Uncheck all tree nodes
  784. @param {optional Object} options
  785. */
  786. Tree.prototype.uncheckAll = function (options) {
  787. var identifiers = this.findNodes('true', 'g', 'state.checked');
  788. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  789. this.setCheckedState(node, false, options);
  790. }, this));
  791. this.render();
  792. };
  793. /**
  794. Uncheck a given tree node
  795. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  796. @param {optional Object} options
  797. */
  798. Tree.prototype.uncheckNode = function (identifiers, options) {
  799. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  800. this.setCheckedState(node, false, options);
  801. }, this));
  802. this.render();
  803. };
  804. /**
  805. Toggles a nodes checked state; checking if unchecked, unchecking if checked.
  806. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  807. @param {optional Object} options
  808. */
  809. Tree.prototype.toggleNodeChecked = function (identifiers, options) {
  810. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  811. this.toggleCheckedState(node, options);
  812. }, this));
  813. this.render();
  814. };
  815. /**
  816. Disable all tree nodes
  817. @param {optional Object} options
  818. */
  819. Tree.prototype.disableAll = function (options) {
  820. var identifiers = this.findNodes('false', 'g', 'state.disabled');
  821. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  822. this.setDisabledState(node, true, options);
  823. }, this));
  824. this.render();
  825. };
  826. /**
  827. Disable a given tree node
  828. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  829. @param {optional Object} options
  830. */
  831. Tree.prototype.disableNode = function (identifiers, options) {
  832. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  833. this.setDisabledState(node, true, options);
  834. }, this));
  835. this.render();
  836. };
  837. /**
  838. Enable all tree nodes
  839. @param {optional Object} options
  840. */
  841. Tree.prototype.enableAll = function (options) {
  842. var identifiers = this.findNodes('true', 'g', 'state.disabled');
  843. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  844. this.setDisabledState(node, false, options);
  845. }, this));
  846. this.render();
  847. };
  848. /**
  849. Enable a given tree node
  850. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  851. @param {optional Object} options
  852. */
  853. Tree.prototype.enableNode = function (identifiers, options) {
  854. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  855. this.setDisabledState(node, false, options);
  856. }, this));
  857. this.render();
  858. };
  859. /**
  860. Toggles a nodes disabled state; disabling is enabled, enabling if disabled.
  861. @param {Object|Number} identifiers - A valid node, node id or array of node identifiers
  862. @param {optional Object} options
  863. */
  864. Tree.prototype.toggleNodeDisabled = function (identifiers, options) {
  865. this.forEachIdentifier(identifiers, options, $.proxy(function (node, options) {
  866. this.setDisabledState(node, !node.state.disabled, options);
  867. }, this));
  868. this.render();
  869. };
  870. /**
  871. Common code for processing multiple identifiers
  872. */
  873. Tree.prototype.forEachIdentifier = function (identifiers, options, callback) {
  874. options = $.extend({}, _default.options, options);
  875. if (!(identifiers instanceof Array)) {
  876. identifiers = [identifiers];
  877. }
  878. $.each(identifiers, $.proxy(function (index, identifier) {
  879. callback(this.identifyNode(identifier), options);
  880. }, this));
  881. };
  882. /*
  883. Identifies a node from either a node id or object
  884. */
  885. Tree.prototype.identifyNode = function (identifier) {
  886. return ((typeof identifier) === 'number') ?
  887. this.nodes[identifier] :
  888. identifier;
  889. };
  890. /**
  891. Searches the tree for nodes (text) that match given criteria
  892. @param {String} pattern - A given string to match against
  893. @param {optional Object} options - Search criteria options
  894. @return {Array} nodes - Matching nodes
  895. */
  896. Tree.prototype.search = function (pattern, options) {
  897. options = $.extend({}, _default.searchOptions, options);
  898. this.clearSearch({ render: false });
  899. var results = [];
  900. if (pattern && pattern.length > 0) {
  901. if (options.exactMatch) {
  902. pattern = '^' + pattern + '$';
  903. }
  904. var modifier = 'g';
  905. if (options.ignoreCase) {
  906. modifier += 'i';
  907. }
  908. results = this.findNodes(pattern, modifier);
  909. // Add searchResult property to all matching nodes
  910. // This will be used to apply custom styles
  911. // and when identifying result to be cleared
  912. $.each(results, function (index, node) {
  913. node.searchResult = true;
  914. })
  915. }
  916. // If revealResults, then render is triggered from revealNode
  917. // otherwise we just call render.
  918. if (options.revealResults) {
  919. this.revealNode(results);
  920. }
  921. else {
  922. this.render();
  923. }
  924. this.$element.trigger('searchComplete', $.extend(true, {}, results));
  925. return results;
  926. };
  927. /**
  928. Clears previous search results
  929. */
  930. Tree.prototype.clearSearch = function (options) {
  931. options = $.extend({}, { render: true }, options);
  932. var results = $.each(this.findNodes('true', 'g', 'searchResult'), function (index, node) {
  933. node.searchResult = false;
  934. });
  935. if (options.render) {
  936. this.render();
  937. }
  938. this.$element.trigger('searchCleared', $.extend(true, {}, results));
  939. };
  940. /**
  941. Find nodes that match a given criteria
  942. @param {String} pattern - A given string to match against
  943. @param {optional String} modifier - Valid RegEx modifiers
  944. @param {optional String} attribute - Attribute to compare pattern against
  945. @return {Array} nodes - Nodes that match your criteria
  946. */
  947. Tree.prototype.findNodes = function (pattern, modifier, attribute) {
  948. modifier = modifier || 'g';
  949. attribute = attribute || 'text';
  950. var _this = this;
  951. return $.grep(this.nodes, function (node) {
  952. var val = _this.getNodeValue(node, attribute);
  953. if (typeof val === 'string') {
  954. return val.match(new RegExp(pattern, modifier));
  955. }
  956. });
  957. };
  958. /**
  959. Recursive find for retrieving nested attributes values
  960. All values are return as strings, unless invalid
  961. @param {Object} obj - Typically a node, could be any object
  962. @param {String} attr - Identifies an object property using dot notation
  963. @return {String} value - Matching attributes string representation
  964. */
  965. Tree.prototype.getNodeValue = function (obj, attr) {
  966. var index = attr.indexOf('.');
  967. if (index > 0) {
  968. var _obj = obj[attr.substring(0, index)];
  969. var _attr = attr.substring(index + 1, attr.length);
  970. return this.getNodeValue(_obj, _attr);
  971. }
  972. else {
  973. if (obj.hasOwnProperty(attr)) {
  974. return obj[attr].toString();
  975. }
  976. else {
  977. return undefined;
  978. }
  979. }
  980. };
  981. var logError = function (message) {
  982. if (window.console) {
  983. window.console.error(message);
  984. }
  985. };
  986. // Prevent against multiple instantiations,
  987. // handle updates and method calls
  988. $.fn[pluginName] = function (options, args) {
  989. var result;
  990. this.each(function () {
  991. var _this = $.data(this, pluginName);
  992. if (typeof options === 'string') {
  993. if (!_this) {
  994. logError('Not initialized, can not call method : ' + options);
  995. }
  996. else if (!$.isFunction(_this[options]) || options.charAt(0) === '_') {
  997. logError('No such method : ' + options);
  998. }
  999. else {
  1000. if (!(args instanceof Array)) {
  1001. args = [ args ];
  1002. }
  1003. result = _this[options].apply(_this, args);
  1004. }
  1005. }
  1006. else if (typeof options === 'boolean') {
  1007. result = _this;
  1008. }
  1009. else {
  1010. $.data(this, pluginName, new Tree(this, $.extend(true, {}, options)));
  1011. }
  1012. });
  1013. return result || this;
  1014. };
  1015. })(jQuery, window, document);