bootstrap-treeview.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445
  1. /* =========================================================
  2. * bootstrap-treeview.js v1.0.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 Tree = function(element, options) {
  24. this.$element = $(element);
  25. this._element = element;
  26. this._elementId = this._element.id;
  27. this._styleId = this._elementId + '-style';
  28. this.tree = [];
  29. this.nodes = [];
  30. this.selectedNode = {};
  31. this._init(options);
  32. };
  33. Tree.defaults = {
  34. injectStyle: true,
  35. levels: 2,
  36. expandIcon: 'glyphicon glyphicon-plus',
  37. collapseIcon: 'glyphicon glyphicon-minus',
  38. nodeIcon: 'glyphicon glyphicon-stop',
  39. color: undefined, // '#000000',
  40. backColor: undefined, // '#FFFFFF',
  41. borderColor: undefined, // '#dddddd',
  42. onhoverColor: '#F5F5F5',
  43. selectedColor: '#FFFFFF',
  44. selectedBackColor: '#428bca',
  45. enableLinks: false,
  46. highlightSelected: true,
  47. showBorder: true,
  48. showTags: false,
  49. // Event handler for when a node is selected
  50. onNodeSelected: undefined
  51. };
  52. Tree.prototype = {
  53. remove: function() {
  54. this._destroy();
  55. $.removeData(this, 'plugin_' + pluginName);
  56. $('#' + this._styleId).remove();
  57. },
  58. _destroy: function() {
  59. if (this.initialized) {
  60. this.$wrapper.remove();
  61. this.$wrapper = null;
  62. // Switch off events
  63. this._unsubscribeEvents();
  64. }
  65. // Reset initialized flag
  66. this.initialized = false;
  67. },
  68. _init: function(options) {
  69. if (options.data) {
  70. if (typeof options.data === 'string') {
  71. options.data = $.parseJSON(options.data);
  72. }
  73. this.tree = $.extend(true, [], options.data);
  74. delete options.data;
  75. }
  76. this.options = $.extend({}, Tree.defaults, options);
  77. this._setInitialLevels(this.tree, 0);
  78. this._destroy();
  79. this._subscribeEvents();
  80. this._render();
  81. },
  82. _unsubscribeEvents: function() {
  83. this.$element.off('click');
  84. },
  85. _subscribeEvents: function() {
  86. this._unsubscribeEvents();
  87. this.$element.on('click', $.proxy(this._clickHandler, this));
  88. if (typeof (this.options.onNodeSelected) === 'function') {
  89. this.$element.on('nodeSelected', this.options.onNodeSelected);
  90. }
  91. },
  92. _clickHandler: function(event) {
  93. if (!this.options.enableLinks) { event.preventDefault(); }
  94. var target = $(event.target),
  95. classList = target.attr('class') ? target.attr('class').split(' ') : [],
  96. node = this._findNode(target);
  97. if ((classList.indexOf('click-expand') != -1) ||
  98. (classList.indexOf('click-collapse') != -1)) {
  99. // Expand or collapse node by toggling child node visibility
  100. this._toggleNodes(node);
  101. this._render();
  102. }
  103. else if (node) {
  104. this._setSelectedNode(node);
  105. }
  106. },
  107. // Looks up the DOM for the closest parent list item to retrieve the
  108. // data attribute nodeid, which is used to lookup the node in the flattened structure.
  109. _findNode: function(target) {
  110. var nodeId = target.closest('li.list-group-item').attr('data-nodeid'),
  111. node = this.nodes[nodeId];
  112. if (!node) {
  113. console.log('Error: node does not exist');
  114. }
  115. return node;
  116. },
  117. // Actually triggers the nodeSelected event
  118. _triggerNodeSelectedEvent: function(node) {
  119. this.$element.trigger('nodeSelected', [$.extend(true, {}, node)]);
  120. },
  121. // Handles selecting and unselecting of nodes,
  122. // as well as determining whether or not to trigger the nodeSelected event
  123. _setSelectedNode: function(node) {
  124. if (!node) { return; }
  125. if (node === this.selectedNode) {
  126. this.selectedNode = {};
  127. }
  128. else {
  129. this._triggerNodeSelectedEvent(this.selectedNode = node);
  130. }
  131. this._render();
  132. },
  133. // On initialization recurses the entire tree structure
  134. // setting expanded / collapsed states based on initial levels
  135. _setInitialLevels: function(nodes, level) {
  136. if (!nodes) { return; }
  137. level += 1;
  138. var self = this;
  139. $.each(nodes, function addNodes(id, node) {
  140. if (level >= self.options.levels) {
  141. self._toggleNodes(node);
  142. }
  143. // Need to traverse both nodes and _nodes to ensure
  144. // all levels collapsed beyond levels
  145. var nodes = node.nodes ? node.nodes : node._nodes ? node._nodes : undefined;
  146. if (nodes) {
  147. return self._setInitialLevels(nodes, level);
  148. }
  149. });
  150. },
  151. // Toggle renaming nodes -> _nodes, _nodes -> nodes
  152. // to simulate expanding or collapsing a node.
  153. _toggleNodes: function(node) {
  154. if (!node.nodes && !node._nodes) {
  155. return;
  156. }
  157. if (node.nodes) {
  158. node._nodes = node.nodes;
  159. delete node.nodes;
  160. }
  161. else {
  162. node.nodes = node._nodes;
  163. delete node._nodes;
  164. }
  165. },
  166. _render: function() {
  167. var self = this;
  168. if (!self.initialized) {
  169. // Setup first time only components
  170. self.$element.addClass(pluginName);
  171. self.$wrapper = $(self._template.list);
  172. self._injectStyle();
  173. self.initialized = true;
  174. }
  175. self.$element.empty().append(self.$wrapper.empty());
  176. // Build tree
  177. self.nodes = [];
  178. self._buildTree(self.tree, 0);
  179. },
  180. // Starting from the root node, and recursing down the
  181. // structure we build the tree one node at a time
  182. _buildTree: function(nodes, level) {
  183. if (!nodes) { return; }
  184. level += 1;
  185. var self = this;
  186. $.each(nodes, function addNodes(id, node) {
  187. node.nodeId = self.nodes.length;
  188. self.nodes.push(node);
  189. var treeItem = $(self._template.item)
  190. .addClass('node-' + self._elementId)
  191. .addClass((node === self.selectedNode) ? 'node-selected' : '')
  192. .attr('data-nodeid', node.nodeId)
  193. .attr('style', self._buildStyleOverride(node));
  194. // Add indent/spacer to mimic tree structure
  195. for (var i = 0; i < (level - 1); i++) {
  196. treeItem.append(self._template.indent);
  197. }
  198. // Add expand, collapse or empty spacer icons
  199. // to facilitate tree structure navigation
  200. if (node._nodes) {
  201. treeItem
  202. .append($(self._template.iconWrapper)
  203. .append($(self._template.icon)
  204. .addClass('click-expand')
  205. .addClass(self.options.expandIcon))
  206. );
  207. }
  208. else if (node.nodes) {
  209. treeItem
  210. .append($(self._template.iconWrapper)
  211. .append($(self._template.icon)
  212. .addClass('click-collapse')
  213. .addClass(self.options.collapseIcon))
  214. );
  215. }
  216. else {
  217. treeItem
  218. .append($(self._template.iconWrapper)
  219. .append($(self._template.icon)
  220. .addClass('glyphicon'))
  221. );
  222. }
  223. // Add node icon
  224. treeItem
  225. .append($(self._template.iconWrapper)
  226. .append($(self._template.icon)
  227. .addClass(node.icon ? node.icon : self.options.nodeIcon))
  228. );
  229. // Add text
  230. if (self.options.enableLinks) {
  231. // Add hyperlink
  232. treeItem
  233. .append($(self._template.link)
  234. .attr('href', node.href)
  235. .append(node.text)
  236. );
  237. }
  238. else {
  239. // otherwise just text
  240. treeItem
  241. .append(node.text);
  242. }
  243. // Add tags as badges
  244. if (self.options.showTags && node.tags) {
  245. $.each(node.tags, function addTag(id, tag) {
  246. treeItem
  247. .append($(self._template.badge)
  248. .append(tag)
  249. );
  250. });
  251. }
  252. // Add item to the tree
  253. self.$wrapper.append(treeItem);
  254. // Recursively add child ndoes
  255. if (node.nodes) {
  256. return self._buildTree(node.nodes, level);
  257. }
  258. });
  259. },
  260. // Define any node level style override for
  261. // 1. selectedNode
  262. // 2. node|data assigned color overrides
  263. _buildStyleOverride: function(node) {
  264. var style = '';
  265. if (this.options.highlightSelected && (node === this.selectedNode)) {
  266. style += 'color:' + this.options.selectedColor + ';';
  267. }
  268. else if (node.color) {
  269. style += 'color:' + node.color + ';';
  270. }
  271. if (this.options.highlightSelected && (node === this.selectedNode)) {
  272. style += 'background-color:' + this.options.selectedBackColor + ';';
  273. }
  274. else if (node.backColor) {
  275. style += 'background-color:' + node.backColor + ';';
  276. }
  277. return style;
  278. },
  279. // Add inline style into head
  280. _injectStyle: function() {
  281. if (this.options.injectStyle && !document.getElementById(this._styleId)) {
  282. $('<style type="text/css" id="' + this._styleId + '"> ' + this._buildStyle() + ' </style>').appendTo('head');
  283. }
  284. },
  285. // Construct trees style based on user options
  286. _buildStyle: function() {
  287. var style = '.node-' + this._elementId + '{';
  288. if (this.options.color) {
  289. style += 'color:' + this.options.color + ';';
  290. }
  291. if (this.options.backColor) {
  292. style += 'background-color:' + this.options.backColor + ';';
  293. }
  294. if (!this.options.showBorder) {
  295. style += 'border:none;';
  296. }
  297. else if (this.options.borderColor) {
  298. style += 'border:1px solid ' + this.options.borderColor + ';';
  299. }
  300. style += '}';
  301. if (this.options.onhoverColor) {
  302. style += '.node-' + this._elementId + ':hover{' +
  303. 'background-color:' + this.options.onhoverColor + ';' +
  304. '}';
  305. }
  306. return this._css + style;
  307. },
  308. _template: {
  309. list: '<ul class="list-group"></ul>',
  310. item: '<li class="list-group-item"></li>',
  311. indent: '<span class="indent"></span>',
  312. iconWrapper: '<span class="icon"></span>',
  313. icon: '<i></i>',
  314. link: '<a href="#" style="color:inherit;"></a>',
  315. badge: '<span class="badge"></span>'
  316. },
  317. _css: '.list-group-item{cursor:pointer;}span.indent{margin-left:10px;margin-right:10px}span.icon{margin-right:5px}'
  318. // _css: '.list-group-item{cursor:pointer;}.list-group-item:hover{background-color:#f5f5f5;}span.indent{margin-left:10px;margin-right:10px}span.icon{margin-right:5px}'
  319. };
  320. var logError = function(message) {
  321. if(window.console) {
  322. window.console.error(message);
  323. }
  324. };
  325. // Prevent against multiple instantiations,
  326. // handle updates and method calls
  327. $.fn[pluginName] = function(options, args) {
  328. return this.each(function() {
  329. var self = $.data(this, 'plugin_' + pluginName);
  330. if (typeof options === 'string') {
  331. if (!self) {
  332. logError('Not initialized, can not call method : ' + options);
  333. }
  334. else if (!$.isFunction(self[options]) || options.charAt(0) === '_') {
  335. logError('No such method : ' + options);
  336. }
  337. else {
  338. if (typeof args === 'string') {
  339. args = [args];
  340. }
  341. self[options].apply(self, args);
  342. }
  343. }
  344. else {
  345. if (!self) {
  346. $.data(this, 'plugin_' + pluginName, new Tree(this, $.extend(true, {}, options)));
  347. }
  348. else {
  349. self._init(options);
  350. }
  351. }
  352. });
  353. };
  354. })(jQuery, window, document);