bootstrap-treeview.js 11 KB

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