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