bootstrap-treeview.js 11 KB

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