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